From d735f6e4728efe92eb087b8837c15eb9dc876b68 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 16 Jul 2025 16:43:26 +0200 Subject: [PATCH 001/117] init --- form/field.go | 172 +++++++++++++++++++++++++++++++++++++++ form/field_input.go | 8 ++ form/field_subform.go | 11 +++ form/form.go | 117 ++++++++++++++++++++++++++ form/option.go | 6 ++ go.mod | 3 + main.go | 77 ++++++++++++++++++ theme/html5.go | 27 ++++++ theme/renderer.go | 90 ++++++++++++++++++++ util/inspect.go | 30 +++++++ validation/constraint.go | 5 ++ validation/error.go | 3 + validation/notblank.go | 46 +++++++++++ validation/validation.go | 13 +++ 14 files changed, 608 insertions(+) create mode 100644 form/field.go create mode 100644 form/field_input.go create mode 100644 form/field_subform.go create mode 100644 form/form.go create mode 100644 form/option.go create mode 100644 go.mod create mode 100644 main.go create mode 100644 theme/html5.go create mode 100644 theme/renderer.go create mode 100644 util/inspect.go create mode 100644 validation/constraint.go create mode 100644 validation/error.go create mode 100644 validation/notblank.go create mode 100644 validation/validation.go diff --git a/form/field.go b/form/field.go new file mode 100644 index 0000000..540de16 --- /dev/null +++ b/form/field.go @@ -0,0 +1,172 @@ +package form + +import ( + "gitnet.fr/deblan/go-form/util" + "gitnet.fr/deblan/go-form/validation" +) + +func FieldValidation(f *Field) bool { + if len(f.Children) > 0 { + isValid := true + + for _, c := range f.Children { + isChildValid, errs := validation.Validate(c.Data, c.Constraints) + + if len(errs) > 0 { + c.Errors = errs + } + + isValid = isValid && isChildValid + } + + return isValid + } else { + isValid, errs := validation.Validate(f.Data, f.Constraints) + f.Errors = []validation.Error{} + + if len(errs) > 0 { + f.Errors = errs + } + + return isValid + } +} + +type Field struct { + Name string + Widget string + Data any + Options []*Option + Children []*Field + Constraints []validation.Constraint + Errors []validation.Error + PrepareView func() map[string]any + BeforeBind func(data any) (any, error) + Validate func(f *Field) bool +} + +func NewField(name, widget string) *Field { + f := &Field{ + Name: name, + Widget: widget, + Data: nil, + } + + f.PrepareView = func() map[string]any { + m := make(map[string]any) + + return m + } + + f.BeforeBind = func(data any) (any, error) { + return data, nil + } + + f.Validate = FieldValidation + + return f +} + +func (f *Field) HasOption(name string) bool { + for _, option := range f.Options { + if option.Name == name { + return true + } + } + + return false +} + +func (f *Field) GetOption(name string) *Option { + for _, option := range f.Options { + if option.Name == name { + return option + } + } + + return nil +} + +func (f *Field) WithOptions(options ...Option) *Field { + for _, option := range options { + if f.HasOption(option.Name) { + f.GetOption(option.Name).Value = option.Value + } else { + f.Options = append(f.Options, &option) + } + } + + return f +} + +func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field { + for _, constraint := range constraints { + f.Constraints = append(f.Constraints, constraint) + } + + return f +} + +func (f *Field) Add(children ...*Field) *Field { + for _, child := range children { + f.Children = append(f.Children, child) + } + + return f +} + +func (f *Field) HasChild(name string) bool { + for _, child := range f.Children { + if name == child.Name { + return true + } + } + + return false +} + +func (f *Field) GetChild(name string) *Field { + var result *Field + + for _, child := range f.Children { + if name == child.Name { + result = child + + break + } + } + + return result +} + +func (f *Field) Bind(data any) error { + if len(f.Children) == 0 { + f.Data = data + + return nil + } + + data, err := f.BeforeBind(data) + + if err != nil { + return err + } + + props, err := util.InspectStruct(data) + + if err != nil { + return err + } + + for key, value := range props { + if f.HasChild(key) { + err = f.GetChild(key).Bind(value) + + if err != nil { + return err + } + } + } + + return nil +} diff --git a/form/field_input.go b/form/field_input.go new file mode 100644 index 0000000..dc7206f --- /dev/null +++ b/form/field_input.go @@ -0,0 +1,8 @@ +package form + +func NewFieldText(name string) *Field { + f := NewField(name, "input"). + WithOptions(Option{Name: "type", Value: "text"}) + + return f +} diff --git a/form/field_subform.go b/form/field_subform.go new file mode 100644 index 0000000..9c5f21c --- /dev/null +++ b/form/field_subform.go @@ -0,0 +1,11 @@ +package form + +func NewFieldSubForm(name string) *Field { + f := NewField(name, "sub_form") + + return f +} + +func NewSubForm(name string) *Field { + return NewFieldSubForm(name) +} diff --git a/form/form.go b/form/form.go new file mode 100644 index 0000000..9cbf600 --- /dev/null +++ b/form/form.go @@ -0,0 +1,117 @@ +package form + +import ( + "net/http" + + "gitnet.fr/deblan/go-form/util" + "gitnet.fr/deblan/go-form/validation" +) + +type Form struct { + Fields []*Field + Errors []validation.Error + Method string + Action string + Name string + Options []Option +} + +func NewForm(fields ...*Field) *Form { + f := new(Form) + f.Method = "POST" + f.Add(fields...) + + return f +} + +func (f *Form) Add(fields ...*Field) { + for _, field := range fields { + f.Fields = append(f.Fields, field) + } +} + +func (f *Form) HasField(name string) bool { + for _, field := range f.Fields { + if name == field.Name { + return true + } + } + + return false +} + +func (f *Form) GetField(name string) *Field { + var result *Field + + for _, field := range f.Fields { + if name == field.Name { + result = field + break + } + } + + return result +} + +func (f *Form) WithMethod(v string) *Form { + f.Method = v + + return f +} + +func (f *Form) WithName(v string) *Form { + f.Name = v + + return f +} + +func (f *Form) WithAction(v string) *Form { + f.Action = v + + return f +} + +func (f *Form) WithOptions(options ...Option) *Form { + for _, option := range options { + f.Options = append(f.Options, option) + } + + return f +} + +func (f *Form) IsValid() bool { + isValid := true + + for _, field := range f.Fields { + fieldIsValid := field.Validate(field) + isValid = isValid && fieldIsValid + } + + return isValid +} + +func (f *Form) Bind(data any) error { + props, err := util.InspectStruct(data) + + if err != nil { + return err + } + + for key, value := range props { + if f.HasField(key) { + err = f.GetField(key).Bind(value) + + if err != nil { + return err + } + } + } + + return nil +} + +func (f *Form) HandleRequest(req http.Request) { + if f.Method == "POST" { + // data := req.PostForm + } +} diff --git a/form/option.go b/form/option.go new file mode 100644 index 0000000..c811db1 --- /dev/null +++ b/form/option.go @@ -0,0 +1,6 @@ +package form + +type Option struct { + Name string + Value any +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f28e114 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitnet.fr/deblan/go-form + +go 1.23.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..71d87b2 --- /dev/null +++ b/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/theme" + "gitnet.fr/deblan/go-form/validation" +) + +func main() { + type Address struct { + Street string + City string + ZipCode uint + } + + type Person struct { + Name string + Address Address + } + + data := new(Person) + data.Name = "" + data.Address = Address{ + Street: "rue des camélias", + City: "", + ZipCode: 39700, + } + + f := form.NewForm( + form.NewFieldText("Name"). + WithOptions( + form.Option{Name: "required", Value: true}, + ). + WithConstraints( + validation.NotBlank{}, + ), + form.NewSubForm("Address"). + Add( + form.NewFieldText("Street"), + form.NewFieldText("City"). + WithConstraints( + validation.NotBlank{}, + ), + form.NewFieldText("ZipCode"), + ), + ).WithMethod("POST").WithAction("") + + f.Bind(data) + + fmt.Printf("%+v\n", f.IsValid()) + + render := theme.NewRenderer(theme.Html5) + v := render.RenderForm(f) + + fmt.Print(v) + + // r, e := theme.RenderForm(f, theme.Html5) + + // fmt.Printf("%+v\n", e) + // fmt.Printf("%+v\n", r) + + // fmt.Printf("%+v\n", e) + // + // fmt.Printf("%+v\n", f) + // + // for _, field := range f.Fields { + // fmt.Printf("%+v\n", *field) + // + // if len(field.Children) > 0 { + // for _, c := range field.Children { + // fmt.Printf("C %+v\n", *c) + // } + // } + // } +} diff --git a/theme/html5.go b/theme/html5.go new file mode 100644 index 0000000..9b50f39 --- /dev/null +++ b/theme/html5.go @@ -0,0 +1,27 @@ +package theme + +var Html5 = map[string]string{ + "form": `
+ {{ form_error .Form nil }} + {{ .Content }} +
`, + "label": ``, + "input": ``, + "sub_form": ` + {{ form_label .Field }} + + {{ range $field := .Field.Children }} + {{ form_row $field }} + {{ end }} + `, + "error": `
+ {{ range $error := .Errors }} + {{ $error }}
+ {{ end }} +
`, + "row": `
+ {{ form_label .Field }} + {{ form_error nil .Field }} + {{ form_widget .Field }} +
`, +} diff --git a/theme/renderer.go b/theme/renderer.go new file mode 100644 index 0000000..a0fc09c --- /dev/null +++ b/theme/renderer.go @@ -0,0 +1,90 @@ +package theme + +import ( + "bytes" + "html/template" + + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/validation" +) + +type Renderer struct { + Theme map[string]string +} + +func NewRenderer(theme map[string]string) *Renderer { + r := new(Renderer) + r.Theme = theme + + return r +} + +func (r *Renderer) RenderForm(form *form.Form) template.HTML { + content := "" + + for _, field := range form.Fields { + content = content + string(r.RenderRow(field)) + } + + return r.Render("form", r.Theme["form"], map[string]any{ + "Form": form, + "Content": template.HTML(content), + }) +} + +func (r *Renderer) RenderRow(field *form.Field) template.HTML { + return r.Render("row", r.Theme["row"], map[string]any{ + "Field": field, + }) +} + +func (r *Renderer) RenderLabel(field *form.Field) template.HTML { + return r.Render("label", r.Theme["label"], map[string]any{ + "Field": field, + }) +} + +func (r *Renderer) RenderWidget(field *form.Field) template.HTML { + return r.Render("widget", r.Theme[field.Widget], map[string]any{ + "Field": field, + }) +} + +func (r *Renderer) RenderError(form *form.Form, field *form.Field) template.HTML { + var errors []validation.Error + + if field != nil { + errors = field.Errors + } + + if form != nil { + errors = form.Errors + } + + return r.Render("error", r.Theme["error"], map[string]any{ + "Errors": errors, + }) +} + +func (r *Renderer) Render(name, tpl string, args any) template.HTML { + t, err := template.New(name).Funcs(template.FuncMap{ + "form": r.RenderForm, + "form_row": r.RenderRow, + "form_label": r.RenderLabel, + "form_widget": r.RenderWidget, + "form_error": r.RenderError, + }).Parse(tpl) + + if err != nil { + return template.HTML(err.Error()) + } + + var buf bytes.Buffer + err = t.Execute(&buf, args) + + if err != nil { + return template.HTML(err.Error()) + } + + return template.HTML(buf.String()) +} diff --git a/util/inspect.go b/util/inspect.go new file mode 100644 index 0000000..6cb1bf7 --- /dev/null +++ b/util/inspect.go @@ -0,0 +1,30 @@ +package util + +import ( + "errors" + "reflect" +) + +func InspectStruct(input interface{}) (map[string]interface{}, error) { + val := reflect.ValueOf(input) + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return nil, errors.New("Invalid type") + } + + result := make(map[string]interface{}) + + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + value := val.Field(i) + + result[field.Name] = value.Interface() + } + + return result, nil +} diff --git a/validation/constraint.go b/validation/constraint.go new file mode 100644 index 0000000..5c42183 --- /dev/null +++ b/validation/constraint.go @@ -0,0 +1,5 @@ +package validation + +type Constraint interface { + Validate(data any) []Error +} diff --git a/validation/error.go b/validation/error.go new file mode 100644 index 0000000..078a221 --- /dev/null +++ b/validation/error.go @@ -0,0 +1,3 @@ +package validation + +type Error string diff --git a/validation/notblank.go b/validation/notblank.go new file mode 100644 index 0000000..250ea31 --- /dev/null +++ b/validation/notblank.go @@ -0,0 +1,46 @@ +package validation + +import ( + "fmt" + "reflect" +) + +type NotBlank struct { +} + +func (c NotBlank) Validate(data any) []Error { + isValid := true + errors := []Error{} + + fmt.Printf("%+v\n", data) + + 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 { + fmt.Printf("d=%+v\n", data) + } + + if !isValid { + errors = append(errors, Error("This value should be blank")) + } + + return errors +} diff --git a/validation/validation.go b/validation/validation.go new file mode 100644 index 0000000..b824d14 --- /dev/null +++ b/validation/validation.go @@ -0,0 +1,13 @@ +package validation + +func Validate(data any, constraints []Constraint) (bool, []Error) { + errs := []Error{} + + for _, constraint := range constraints { + for _, e := range constraint.Validate(data) { + errs = append(errs, e) + } + } + + return len(errs) == 0, errs +} From 3894fb31e96c8ba11a144cd18158b3005c4e902b Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 16 Jul 2025 19:04:31 +0200 Subject: [PATCH 002/117] feat: improve form rendering --- .gitignore | 1 + example/address.go | 48 ++++++++++++++++++++++++ form/field.go | 29 +++++++++++++++ form/field_input.go | 18 +++++++++ form/field_textarea.go | 5 +++ form/form.go | 83 +++++++++++++++++++++++++++++++++++++----- main.go | 77 ++++++++++++++------------------------- theme/html5.go | 63 ++++++++++++++++++++++---------- theme/renderer.go | 65 +++++++++++++++++++++++++++++---- validation/notblank.go | 6 +-- 10 files changed, 303 insertions(+), 92 deletions(-) create mode 100644 .gitignore create mode 100644 example/address.go create mode 100644 form/field_textarea.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5bebc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/go-form diff --git a/example/address.go b/example/address.go new file mode 100644 index 0000000..61bd796 --- /dev/null +++ b/example/address.go @@ -0,0 +1,48 @@ +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.Option{Name: "label", Value: "Name"}, + form.Option{Name: "required", Value: true}, + ). + WithConstraints( + validation.NotBlank{}, + ), + form.NewSubForm("Address"). + WithOptions(form.Option{Name: "label", Value: "Address"}). + Add( + form.NewFieldTextarea("Street"). + WithOptions(form.Option{Name: "label", Value: "Street"}). + WithConstraints( + validation.NotBlank{}, + ), + form.NewFieldText("City"). + WithOptions(form.Option{Name: "label", Value: "City"}). + WithConstraints( + validation.NotBlank{}, + ), + form.NewFieldNumber("ZipCode"). + WithOptions(form.Option{Name: "label", Value: "Zip code"}). + WithConstraints( + validation.NotBlank{}, + ), + ), + form.NewSubmit("submit"), + ). + End(). + WithMethod("POST"). + // WithMethod("GET"). + WithAction("/"). + WithOptions( + form.Option{Name: "attr", Value: map[string]string{ + "id": "my-form", + }}, + ) +} diff --git a/form/field.go b/form/field.go index 540de16..3d755f1 100644 --- a/form/field.go +++ b/form/field.go @@ -1,6 +1,9 @@ package form import ( + "fmt" + "strings" + "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) @@ -43,6 +46,8 @@ type Field struct { PrepareView func() map[string]any BeforeBind func(data any) (any, error) Validate func(f *Field) bool + Form *Form + Parent *Field } func NewField(name, widget string) *Field { @@ -109,6 +114,7 @@ func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field { func (f *Field) Add(children ...*Field) *Field { for _, child := range children { + child.Parent = f f.Children = append(f.Children, child) } @@ -139,6 +145,29 @@ func (f *Field) GetChild(name string) *Field { return result } +func (f *Field) GetName() string { + var name string + + if f.Form != nil && f.Form.Name != "" { + name = fmt.Sprintf("%s[%s]", f.Form.Name, f.Name) + } else if f.Parent != nil { + name = fmt.Sprintf("%s[%s]", f.Parent.GetName(), f.Name) + } else { + name = f.Name + } + + return name +} + +func (f *Field) GetId() string { + name := f.GetName() + name = strings.ReplaceAll(name, "[", "-") + name = strings.ReplaceAll(name, "]", "") + name = strings.ToLower(name) + + return name +} + func (f *Field) Bind(data any) error { if len(f.Children) == 0 { f.Data = data diff --git a/form/field_input.go b/form/field_input.go index dc7206f..3c9e2d1 100644 --- a/form/field_input.go +++ b/form/field_input.go @@ -6,3 +6,21 @@ func NewFieldText(name string) *Field { return f } + +func NewFieldNumber(name string) *Field { + f := NewField(name, "input"). + WithOptions(Option{Name: "type", Value: "number"}) + + return f +} + +func NewSubmit(name string) *Field { + f := NewField(name, "input"). + WithOptions( + Option{Name: "type", Value: "submit"}, + ) + + f.Data = "Submit" + + return f +} diff --git a/form/field_textarea.go b/form/field_textarea.go new file mode 100644 index 0000000..c32f704 --- /dev/null +++ b/form/field_textarea.go @@ -0,0 +1,5 @@ +package form + +func NewFieldTextarea(name string) *Field { + return NewField(name, "textarea") +} diff --git a/form/form.go b/form/form.go index 9cbf600..4405304 100644 --- a/form/form.go +++ b/form/form.go @@ -2,34 +2,75 @@ package form import ( "net/http" + "net/url" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) type Form struct { - Fields []*Field - Errors []validation.Error - Method string - Action string - Name string - Options []Option + Fields []*Field + GlobalFields []*Field + Errors []validation.Error + Method string + Action string + Name string + Options []*Option + RequestData *url.Values } func NewForm(fields ...*Field) *Form { f := new(Form) f.Method = "POST" + f.Name = "form" f.Add(fields...) return f } +func (f *Form) HasOption(name string) bool { + for _, option := range f.Options { + if option.Name == name { + return true + } + } + + return false +} + +func (f *Form) GetOption(name string) *Option { + for _, option := range f.Options { + if option.Name == name { + return option + } + } + + return nil +} + func (f *Form) Add(fields ...*Field) { for _, field := range fields { + field.Form = f f.Fields = append(f.Fields, field) } } +func (f *Form) End() *Form { + for _, c := range f.Fields { + f.AddGlobalField(c) + } + + return f +} + +func (f *Form) AddGlobalField(field *Field) { + f.GlobalFields = append(f.GlobalFields, field) + + for _, c := range field.Children { + f.AddGlobalField(c) + } +} + func (f *Form) HasField(name string) bool { for _, field := range f.Fields { if name == field.Name { @@ -73,7 +114,7 @@ func (f *Form) WithAction(v string) *Form { func (f *Form) WithOptions(options ...Option) *Form { for _, option := range options { - f.Options = append(f.Options, option) + f.Options = append(f.Options, &option) } return f @@ -110,8 +151,30 @@ func (f *Form) Bind(data any) error { return nil } -func (f *Form) HandleRequest(req http.Request) { - if f.Method == "POST" { - // data := req.PostForm +func (f *Form) HandleRequest(req *http.Request) { + var data url.Values + + if f.Method != "GET" { + req.ParseForm() + data = req.Form + } else { + data = req.URL.Query() + } + + isSubmitted := false + + for _, c := range f.GlobalFields { + if data.Has(c.GetName()) { + isSubmitted = true + c.Bind(data.Get(c.GetName())) + } + } + + if isSubmitted { + f.RequestData = &data } } + +func (f *Form) IsSubmitted() bool { + return f.RequestData != nil +} diff --git a/main.go b/main.go index 71d87b2..8ce6fc5 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,11 @@ package main import ( "fmt" + "log" + "net/http" - "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/example" "gitnet.fr/deblan/go-form/theme" - "gitnet.fr/deblan/go-form/validation" ) func main() { @@ -20,58 +21,34 @@ func main() { Address Address } - data := new(Person) - data.Name = "" - data.Address = Address{ - Street: "rue des camélias", - City: "", - ZipCode: 39700, - } + 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, + } - f := form.NewForm( - form.NewFieldText("Name"). - WithOptions( - form.Option{Name: "required", Value: true}, - ). - WithConstraints( - validation.NotBlank{}, - ), - form.NewSubForm("Address"). - Add( - form.NewFieldText("Street"), - form.NewFieldText("City"). - WithConstraints( - validation.NotBlank{}, - ), - form.NewFieldText("ZipCode"), - ), - ).WithMethod("POST").WithAction("") + f := example.CreateAddressForm() + f.Bind(data) - f.Bind(data) + if r.Method == f.Method { + f.HandleRequest(r) - fmt.Printf("%+v\n", f.IsValid()) + if f.IsSubmitted() && f.IsValid() { + fmt.Printf("%+v\n", "OK") + } else { + fmt.Printf("%+v\n", "KO") + } + } - render := theme.NewRenderer(theme.Html5) - v := render.RenderForm(f) + render := theme.NewRenderer(theme.Html5) + v := render.RenderForm(f) - fmt.Print(v) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(v)) + }) - // r, e := theme.RenderForm(f, theme.Html5) - - // fmt.Printf("%+v\n", e) - // fmt.Printf("%+v\n", r) - - // fmt.Printf("%+v\n", e) - // - // fmt.Printf("%+v\n", f) - // - // for _, field := range f.Fields { - // fmt.Printf("%+v\n", *field) - // - // if len(field.Children) > 0 { - // for _, c := range field.Children { - // fmt.Printf("C %+v\n", *c) - // } - // } - // } + log.Fatal(http.ListenAndServe(":1122", nil)) } diff --git a/theme/html5.go b/theme/html5.go index 9b50f39..1d100ac 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -1,27 +1,52 @@ package theme var Html5 = map[string]string{ - "form": `
- {{ form_error .Form nil }} - {{ .Content }} -
`, - "label": ``, - "input": ``, - "sub_form": ` - {{ form_label .Field }} + "form": `
+ {{- form_error .Form nil -}} - {{ range $field := .Field.Children }} - {{ form_row $field }} - {{ end }} + {{- range $field := .Form.Fields -}} + {{- form_row $field -}} + {{- end -}} +
`, + "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`, + // "attributes": `{{ if gt (len .Attributes) 0 }} + // {{ range $key, $value := .Attributes }} + // {{ $key }}="{{ $value }}" + // {{ end }} + // {{ end }}`, + "label": ` + {{ if .Field.HasOption "label" }} + {{ $label := (.Field.GetOption "label").Value }} + + {{- if ne $label "" -}} + + {{- end -}} + {{- end -}} + `, + "input": ` + {{ $type := .Field.GetOption "type" }} + + `, + "textarea": ` + + `, + "sub_form": ` + {{- range $field := .Field.Children -}} + {{- form_row $field -}} + {{- end -}} + `, + "error": ` + {{- if gt (len .Errors) 0 -}} + + {{- end -}} `, - "error": `
- {{ range $error := .Errors }} - {{ $error }}
- {{ end }} -
`, "row": `
- {{ form_label .Field }} - {{ form_error nil .Field }} - {{ form_widget .Field }} + {{- form_label .Field -}} + {{- form_error nil .Field -}} + {{- form_widget .Field -}}
`, } diff --git a/theme/renderer.go b/theme/renderer.go index a0fc09c..6a89a5a 100644 --- a/theme/renderer.go +++ b/theme/renderer.go @@ -20,15 +20,8 @@ func NewRenderer(theme map[string]string) *Renderer { } func (r *Renderer) RenderForm(form *form.Form) template.HTML { - content := "" - - for _, field := range form.Fields { - content = content + string(r.RenderRow(field)) - } - return r.Render("form", r.Theme["form"], map[string]any{ - "Form": form, - "Content": template.HTML(content), + "Form": form, }) } @@ -66,6 +59,59 @@ func (r *Renderer) RenderError(form *form.Form, field *form.Field) template.HTML }) } +func (r *Renderer) RenderLabelAttr(field *form.Field) template.HTMLAttr { + var attributes map[string]string + + if field.HasOption("label_attr") { + attributes = field.GetOption("label_attr").Value.(map[string]string) + } + + return r.RenderAttr("label_attr", r.Theme["attributes"], map[string]any{ + "Attributes": attributes, + }) +} + +func (r *Renderer) RenderWidgetAttr(field *form.Field) template.HTMLAttr { + var attributes map[string]string + + if field.HasOption("attr") { + attributes = field.GetOption("attr").Value.(map[string]string) + } + + return r.RenderAttr("widget_attr", r.Theme["attributes"], map[string]any{ + "Attributes": attributes, + }) +} + +func (r *Renderer) RenderFormAttr(form *form.Form) template.HTMLAttr { + var attributes map[string]string + + if form.HasOption("attr") { + attributes = form.GetOption("attr").Value.(map[string]string) + } + + return r.RenderAttr("form_attr", r.Theme["attributes"], map[string]any{ + "Attributes": attributes, + }) +} + +func (r *Renderer) RenderAttr(name, tpl string, args any) template.HTMLAttr { + t, err := template.New(name).Parse(tpl) + + if err != nil { + return template.HTMLAttr("") + } + + var buf bytes.Buffer + err = t.Execute(&buf, args) + + if err != nil { + return 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{ "form": r.RenderForm, @@ -73,6 +119,9 @@ func (r *Renderer) Render(name, tpl string, args any) template.HTML { "form_label": r.RenderLabel, "form_widget": r.RenderWidget, "form_error": r.RenderError, + "form_attr": r.RenderFormAttr, + "widget_attr": r.RenderWidgetAttr, + "label_attr": r.RenderLabelAttr, }).Parse(tpl) if err != nil { diff --git a/validation/notblank.go b/validation/notblank.go index 250ea31..e14ede7 100644 --- a/validation/notblank.go +++ b/validation/notblank.go @@ -1,7 +1,6 @@ package validation import ( - "fmt" "reflect" ) @@ -11,9 +10,6 @@ type NotBlank struct { func (c NotBlank) Validate(data any) []Error { isValid := true errors := []Error{} - - fmt.Printf("%+v\n", data) - t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { @@ -35,7 +31,7 @@ func (c NotBlank) Validate(data any) []Error { isValid = false } } else { - fmt.Printf("d=%+v\n", data) + errors = append(errors, Error("This value can not be processed")) } if !isValid { From cef8567ad3867bbe9830f943eed9fd3850867c47 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 16 Jul 2025 22:37:15 +0200 Subject: [PATCH 003/117] feat: add data binding feat: add help --- example/address.go | 21 +++++++++++++-------- form/field.go | 41 ++++++++++++++++++++++++++++++++++++----- form/field_input.go | 14 +++++++++++--- form/form.go | 23 ++++++++++++++++++----- form/option.go | 7 +++++++ go.mod | 2 ++ go.sum | 2 ++ main.go | 7 ++----- theme/html5.go | 25 +++++++++++++++---------- theme/renderer.go | 42 ++++++++++++++++++++++++++++++++++-------- 10 files changed, 140 insertions(+), 44 deletions(-) create mode 100644 go.sum diff --git a/example/address.go b/example/address.go index 61bd796..ef58cf6 100644 --- a/example/address.go +++ b/example/address.go @@ -9,27 +9,31 @@ func CreateAddressForm() *form.Form { return form.NewForm( form.NewFieldText("Name"). WithOptions( - form.Option{Name: "label", Value: "Name"}, - form.Option{Name: "required", Value: true}, + form.NewOption("label", "Name"), + form.NewOption("required", true), + form.NewOption("help", "A help!"), ). WithConstraints( validation.NotBlank{}, ), form.NewSubForm("Address"). - WithOptions(form.Option{Name: "label", Value: "Address"}). + WithOptions(form.NewOption("label", "Address")). Add( form.NewFieldTextarea("Street"). - WithOptions(form.Option{Name: "label", Value: "Street"}). + WithOptions(form.NewOption("label", "Street")). WithConstraints( validation.NotBlank{}, ), form.NewFieldText("City"). - WithOptions(form.Option{Name: "label", Value: "City"}). + WithOptions(form.NewOption("label", "City")). WithConstraints( validation.NotBlank{}, ), form.NewFieldNumber("ZipCode"). - WithOptions(form.Option{Name: "label", Value: "Zip code"}). + WithOptions( + form.NewOption("label", "Zip code"), + form.NewOption("help", "A field help"), + ). WithConstraints( validation.NotBlank{}, ), @@ -41,8 +45,9 @@ func CreateAddressForm() *form.Form { // WithMethod("GET"). WithAction("/"). WithOptions( - form.Option{Name: "attr", Value: map[string]string{ + 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 3d755f1..b96980e 100644 --- a/form/field.go +++ b/form/field.go @@ -44,6 +44,7 @@ type Field struct { Constraints []validation.Constraint Errors []validation.Error PrepareView func() map[string]any + BeforeMount func(data any) (any, error) BeforeBind func(data any) (any, error) Validate func(f *Field) bool Form *Form @@ -63,6 +64,10 @@ func NewField(name, widget string) *Field { return m } + f.BeforeMount = func(data any) (any, error) { + return data, nil + } + f.BeforeBind = func(data any) (any, error) { return data, nil } @@ -92,12 +97,12 @@ func (f *Field) GetOption(name string) *Option { return nil } -func (f *Field) WithOptions(options ...Option) *Field { +func (f *Field) WithOptions(options ...*Option) *Field { for _, option := range options { if f.HasOption(option.Name) { f.GetOption(option.Name).Value = option.Value } else { - f.Options = append(f.Options, &option) + f.Options = append(f.Options, option) } } @@ -168,14 +173,14 @@ func (f *Field) GetId() string { return name } -func (f *Field) Bind(data any) error { +func (f *Field) Mount(data any) error { if len(f.Children) == 0 { f.Data = data return nil } - data, err := f.BeforeBind(data) + data, err := f.BeforeMount(data) if err != nil { return err @@ -189,7 +194,7 @@ func (f *Field) Bind(data any) error { for key, value := range props { if f.HasChild(key) { - err = f.GetChild(key).Bind(value) + err = f.GetChild(key).Mount(value) if err != nil { return err @@ -199,3 +204,29 @@ func (f *Field) Bind(data any) error { return nil } + +func (f *Field) Bind(data map[string]any, key *string) error { + if len(f.Children) == 0 { + v, err := f.BeforeBind(f.Data) + + if err != nil { + return err + } + + if key != nil { + data[*key] = v + } else { + data[f.Name] = v + } + + return nil + } + + data[f.Name] = make(map[string]any) + + for _, child := range f.Children { + child.Bind(data[f.Name].(map[string]any), key) + } + + return nil +} diff --git a/form/field_input.go b/form/field_input.go index 3c9e2d1..2b5ee58 100644 --- a/form/field_input.go +++ b/form/field_input.go @@ -1,15 +1,23 @@ package form +import ( + "github.com/spf13/cast" +) + func NewFieldText(name string) *Field { f := NewField(name, "input"). - WithOptions(Option{Name: "type", Value: "text"}) + WithOptions(NewOption("type", "text")) return f } func NewFieldNumber(name string) *Field { f := NewField(name, "input"). - WithOptions(Option{Name: "type", Value: "number"}) + WithOptions(NewOption("type", "number")) + + f.BeforeBind = func(data any) (any, error) { + return cast.ToFloat64(data), nil + } return f } @@ -17,7 +25,7 @@ func NewFieldNumber(name string) *Field { func NewSubmit(name string) *Field { f := NewField(name, "input"). WithOptions( - Option{Name: "type", Value: "submit"}, + NewOption("type", "submit"), ) f.Data = "Submit" diff --git a/form/form.go b/form/form.go index 4405304..3c218af 100644 --- a/form/form.go +++ b/form/form.go @@ -1,6 +1,7 @@ package form import ( + "encoding/json" "net/http" "net/url" @@ -112,9 +113,9 @@ func (f *Form) WithAction(v string) *Form { return f } -func (f *Form) WithOptions(options ...Option) *Form { +func (f *Form) WithOptions(options ...*Option) *Form { for _, option := range options { - f.Options = append(f.Options, &option) + f.Options = append(f.Options, option) } return f @@ -131,7 +132,7 @@ func (f *Form) IsValid() bool { return isValid } -func (f *Form) Bind(data any) error { +func (f *Form) Mount(data any) error { props, err := util.InspectStruct(data) if err != nil { @@ -140,7 +141,7 @@ func (f *Form) Bind(data any) error { for key, value := range props { if f.HasField(key) { - err = f.GetField(key).Bind(value) + err = f.GetField(key).Mount(value) if err != nil { return err @@ -151,6 +152,18 @@ func (f *Form) Bind(data any) error { return nil } +func (f *Form) Bind(data any) error { + toBind := make(map[string]any) + + for _, field := range f.Fields { + field.Bind(toBind, nil) + } + + j, _ := json.Marshal(toBind) + + return json.Unmarshal(j, data) +} + func (f *Form) HandleRequest(req *http.Request) { var data url.Values @@ -166,7 +179,7 @@ func (f *Form) HandleRequest(req *http.Request) { for _, c := range f.GlobalFields { if data.Has(c.GetName()) { isSubmitted = true - c.Bind(data.Get(c.GetName())) + c.Mount(data.Get(c.GetName())) } } diff --git a/form/option.go b/form/option.go index c811db1..db9b86a 100644 --- a/form/option.go +++ b/form/option.go @@ -4,3 +4,10 @@ type Option struct { Name string Value any } + +func NewOption(name string, value any) *Option { + return &Option{ + Name: name, + Value: value, + } +} diff --git a/go.mod b/go.mod index f28e114..4377854 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module gitnet.fr/deblan/go-form go 1.23.0 + +require github.com/spf13/cast v1.9.2 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a02d85d --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +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 8ce6fc5..7b70eaa 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "log" "net/http" @@ -31,15 +30,13 @@ func main() { } f := example.CreateAddressForm() - f.Bind(data) + f.Mount(data) if r.Method == f.Method { f.HandleRequest(r) if f.IsSubmitted() && f.IsValid() { - fmt.Printf("%+v\n", "OK") - } else { - fmt.Printf("%+v\n", "KO") + f.Bind(&data) } } diff --git a/theme/html5.go b/theme/html5.go index 1d100ac..4ea7d60 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -4,42 +4,46 @@ var Html5 = map[string]string{ "form": `
{{- form_error .Form nil -}} + {{- form_help .Form -}} + {{- range $field := .Form.Fields -}} {{- form_row $field -}} {{- end -}}
`, "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`, - // "attributes": `{{ if gt (len .Attributes) 0 }} - // {{ range $key, $value := .Attributes }} - // {{ $key }}="{{ $value }}" - // {{ end }} - // {{ end }}`, + "help": ` + {{- if gt (len .Help) 0 -}} +
{{ .Help }}
+ {{- end -}} + `, "label": ` {{ if .Field.HasOption "label" }} {{ $label := (.Field.GetOption "label").Value }} {{- if ne $label "" -}} - + {{- end -}} {{- end -}} `, "input": ` {{ $type := .Field.GetOption "type" }} - + `, "textarea": ` - + `, "sub_form": ` + {{ form_widget_help .Field }} + {{- range $field := .Field.Children -}} {{- form_row $field -}} {{- end -}} `, "error": ` {{- if gt (len .Errors) 0 -}} -
    +
      {{- range $error := .Errors -}} -
    • {{- $error -}}
    • +
    • {{- $error -}}
    • {{- end -}}
    {{- end -}} @@ -48,5 +52,6 @@ var Html5 = map[string]string{ {{- form_label .Field -}} {{- form_error nil .Field -}} {{- form_widget .Field -}} + {{- form_widget_help .Field -}} `, } diff --git a/theme/renderer.go b/theme/renderer.go index 6a89a5a..08e841c 100644 --- a/theme/renderer.go +++ b/theme/renderer.go @@ -95,6 +95,30 @@ func (r *Renderer) RenderFormAttr(form *form.Form) template.HTMLAttr { }) } +func (r *Renderer) RenderFormHelp(form *form.Form) template.HTML { + var help string + + if form.HasOption("help") { + help = form.GetOption("help").Value.(string) + } + + return r.Render("help", r.Theme["help"], map[string]any{ + "Help": help, + }) +} + +func (r *Renderer) RenderWidgetHelp(field *form.Field) template.HTML { + var help string + + if field.HasOption("help") { + help = field.GetOption("help").Value.(string) + } + + return r.Render("help", r.Theme["help"], map[string]any{ + "Help": help, + }) +} + func (r *Renderer) RenderAttr(name, tpl string, args any) template.HTMLAttr { t, err := template.New(name).Parse(tpl) @@ -114,14 +138,16 @@ func (r *Renderer) RenderAttr(name, tpl string, args any) template.HTMLAttr { func (r *Renderer) Render(name, tpl string, args any) template.HTML { t, err := template.New(name).Funcs(template.FuncMap{ - "form": r.RenderForm, - "form_row": r.RenderRow, - "form_label": r.RenderLabel, - "form_widget": r.RenderWidget, - "form_error": r.RenderError, - "form_attr": r.RenderFormAttr, - "widget_attr": r.RenderWidgetAttr, - "label_attr": r.RenderLabelAttr, + "form": r.RenderForm, + "form_row": r.RenderRow, + "form_label": r.RenderLabel, + "form_widget": r.RenderWidget, + "form_error": r.RenderError, + "form_attr": r.RenderFormAttr, + "form_widget_attr": r.RenderWidgetAttr, + "form_label_attr": r.RenderLabelAttr, + "form_help": r.RenderFormHelp, + "form_widget_help": r.RenderWidgetHelp, }).Parse(tpl) if err != nil { From b7c2ddeebf515974010f806cf7f5cbd4311a2a12 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Thu, 17 Jul 2025 23:11:36 +0200 Subject: [PATCH 004/117] 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 --- example/address.go | 53 --------------- example/form.go | 135 ++++++++++++++++++++++++++++++++++++++ form/field.go | 40 ++++++++++-- form/field_checkbox.go | 32 +++++++++ form/field_choice.go | 137 +++++++++++++++++++++++++++++++++++++++ form/field_input.go | 35 ++++++++-- form/field_input_date.go | 90 +++++++++++++++++++++++++ form/form.go | 20 ++++-- go.mod | 5 +- go.sum | 2 + main.go | 94 +++++++++++++++++++++++---- theme/html5.go | 76 ++++++++++++++++++++-- theme/renderer.go | 19 +++++- validation/mail.go | 25 +++++++ validation/notblank.go | 51 ++++++++++----- 15 files changed, 702 insertions(+), 112 deletions(-) delete mode 100644 example/address.go create mode 100644 example/form.go create mode 100644 form/field_checkbox.go create mode 100644 form/field_choice.go create mode 100644 form/field_input_date.go create mode 100644 validation/mail.go 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(` + + + + + Form + + + + {{ form .Form }} + + + `) 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)) diff --git a/theme/html5.go b/theme/html5.go index 4ea7d60..2e1055a 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -26,18 +26,70 @@ var Html5 = map[string]string{ {{- end -}} `, "input": ` - {{ $type := .Field.GetOption "type" }} - + {{- $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 -}} + + `, "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) -}} + + + {{- end -}} + + {{- range $key, $choice := $choices.GetChoices -}} + + + {{- end -}} + {{- else -}} + + {{- end -}} + `, + "sub_form": ` +
    + {{ if .Field.HasOption "label" }} + {{ $label := (.Field.GetOption "label").Value }} + + {{- if ne $label "" -}} + {{ $label }} + {{- end -}} + {{- end -}} + + {{ form_widget_help .Field }} + + {{- range $field := .Field.Children -}} + {{- form_row $field -}} + {{- end -}} +
    `, "error": ` {{- if gt (len .Errors) 0 -}} @@ -49,9 +101,19 @@ var Html5 = map[string]string{ {{- end -}} `, "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 -}}
    `, } diff --git a/theme/renderer.go b/theme/renderer.go index 08e841c..d43edc7 100644 --- a/theme/renderer.go +++ b/theme/renderer.go @@ -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()) diff --git a/validation/mail.go b/validation/mail.go new file mode 100644 index 0000000..d999dc7 --- /dev/null +++ b/validation/mail.go @@ -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 +} diff --git a/validation/notblank.go b/validation/notblank.go index e14ede7..45460b9 100644 --- a/validation/notblank.go +++ b/validation/notblank.go @@ -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 From f2abb4326151466d5fbbe8019def2e64a5609afb Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Jul 2025 15:21:30 +0200 Subject: [PATCH 005/117] feat: add constraints (length, range, regex) --- validation/length.go | 95 ++++++++++++++++++++++++++++++++++++++++ validation/range.go | 102 +++++++++++++++++++++++++++++++++++++++++++ validation/regex.go | 64 +++++++++++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 validation/length.go create mode 100644 validation/range.go create mode 100644 validation/regex.go diff --git a/validation/length.go b/validation/length.go new file mode 100644 index 0000000..d54bd0d --- /dev/null +++ b/validation/length.go @@ -0,0 +1,95 @@ +package validation + +import ( + "reflect" + "strings" + + "github.com/spf13/cast" +) + +type Length struct { + Min *int + Max *int + MinMessage string + MaxMessage string + ExactMessage string + TypeErrorMessage string +} + +func NewLength() Length { + return Length{ + MinMessage: "This value is too short (min: {{ min }}).", + MaxMessage: "This value is too long (max: {{ max }}).", + ExactMessage: "This value is not valid (expected: {{ min }}).", + TypeErrorMessage: "This value can not be processed.", + } +} + +func (c Length) WithMin(v int) Length { + c.Min = &v + + return c +} + +func (c Length) WithMax(v int) Length { + c.Max = &v + + return c +} + +func (c Length) WithExact(v int) Length { + c.Min = &v + c.Max = &v + + return c +} + +func (c Length) Validate(data any) []Error { + if c.Min == nil && c.Max == nil { + return []Error{} + } + + errors := []Error{} + + t := reflect.TypeOf(data) + + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + var size *int + + switch t.Kind() { + case reflect.Array: + case reflect.Slice: + s := reflect.ValueOf(data).Len() + size = &s + case reflect.String: + s := len(data.(string)) + size = &s + + default: + errors = append(errors, Error(c.TypeErrorMessage)) + } + + if size != nil { + if c.Max != nil && c.Min != nil { + if *c.Max == *c.Min && *size != *c.Max { + errors = append(errors, Error(c.BuildMessage(c.ExactMessage))) + } + } else if c.Min != nil && *size < *c.Min { + errors = append(errors, Error(c.BuildMessage(c.MinMessage))) + } else if c.Max != nil && *size > *c.Max { + errors = append(errors, Error(c.BuildMessage(c.MaxMessage))) + } + } + + return errors +} + +func (c *Length) BuildMessage(message string) string { + message = strings.ReplaceAll(message, "{{ min }}", cast.ToString(c.Min)) + message = strings.ReplaceAll(message, "{{ max }}", cast.ToString(c.Max)) + + return message +} diff --git a/validation/range.go b/validation/range.go new file mode 100644 index 0000000..0bbdb65 --- /dev/null +++ b/validation/range.go @@ -0,0 +1,102 @@ +package validation + +import ( + "reflect" + "strings" + + "github.com/spf13/cast" +) + +type Range struct { + Min *float64 + Max *float64 + MinMessage string + MaxMessage string + RangeMessage string + TypeErrorMessage string +} + +func NewRange() Range { + return Range{ + MinMessage: "This value must be greater than or equal to {{ min }}.", + MaxMessage: "This value must be less than or equal to {{ max }}.", + RangeMessage: "This value should be between {{ min }} and {{ max }}.", + TypeErrorMessage: "This value can not be processed.", + } +} + +func (c Range) WithMin(v float64) Range { + c.Min = &v + + return c +} + +func (c Range) WithMax(v float64) Range { + c.Max = &v + + return c +} + +func (c Range) WithRange(vMin, vMax float64) Range { + c.Min = &vMin + c.Max = &vMax + + return c +} + +func (c Range) Validate(data any) []Error { + if c.Min == nil && c.Max == nil { + return []Error{} + } + + errors := []Error{} + + t := reflect.TypeOf(data) + + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + switch t.Kind() { + 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: + case reflect.String: + isValidMin := c.Min == nil || *c.Min <= cast.ToFloat64(data.(string)) + isValidMax := c.Max == nil || *c.Max >= cast.ToFloat64(data.(string)) + + if !isValidMin || !isValidMax { + errors = append(errors, Error(c.BuildMessage())) + } + default: + errors = append(errors, Error(c.TypeErrorMessage)) + } + + return errors +} + +func (c *Range) BuildMessage() string { + var message string + + if c.Min != nil && c.Max == nil { + message = c.MinMessage + } else if c.Max != nil && c.Min == nil { + message = c.MaxMessage + } else { + message = c.RangeMessage + } + + message = strings.ReplaceAll(message, "{{ min }}", cast.ToString(c.Min)) + message = strings.ReplaceAll(message, "{{ max }}", cast.ToString(c.Max)) + + return message +} diff --git a/validation/regex.go b/validation/regex.go new file mode 100644 index 0000000..5dc6f8e --- /dev/null +++ b/validation/regex.go @@ -0,0 +1,64 @@ +package validation + +import ( + "reflect" + "regexp" +) + +type Regex struct { + Message string + TypeErrorMessage string + Match bool + Expression string +} + +func NewRegex(expr string) Regex { + return Regex{ + Message: "This value is not valid.", + TypeErrorMessage: "This value can not be processed.", + Match: true, + Expression: expr, + } +} + +func (c Regex) MustMatch() Regex { + c.Match = true + + return c +} + +func (c Regex) MustNotMatch() Regex { + c.Match = false + + return c +} + +func (c Regex) Validate(data any) []Error { + errors := []Error{} + notBlank := NotBlank{} + nbErrs := notBlank.Validate(data) + + if len(nbErrs) > 0 { + return errors + } + + t := reflect.TypeOf(data) + + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + switch t.Kind() { + case reflect.String: + matched, _ := regexp.MatchString(c.Expression, data.(string)) + + if !matched && c.Match || matched && !c.Match { + errors = append(errors, Error(c.Message)) + } + + default: + errors = append(errors, Error(c.TypeErrorMessage)) + } + + return errors +} From 287acfc4d2454f95cc75a404580e16a16f0af991 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Jul 2025 15:21:46 +0200 Subject: [PATCH 006/117] feat: add csrf input --- form/field_input_csrf.go | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 form/field_input_csrf.go diff --git a/form/field_input_csrf.go b/form/field_input_csrf.go new file mode 100644 index 0000000..0b25ece --- /dev/null +++ b/form/field_input_csrf.go @@ -0,0 +1,8 @@ +package form + +func NewFieldCsrf(name string) *Field { + f := NewFieldHidden(name). + WithFixedName() + + return f +} From 1f232437dbeca22f7ee76d397d82b340c240dc58 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Jul 2025 15:22:00 +0200 Subject: [PATCH 007/117] feat: add boostrap5 theme --- theme/bootstrap5.go | 140 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 theme/bootstrap5.go diff --git a/theme/bootstrap5.go b/theme/bootstrap5.go new file mode 100644 index 0000000..34d697e --- /dev/null +++ b/theme/bootstrap5.go @@ -0,0 +1,140 @@ +package theme + +var Bootstrap5 = map[string]string{ + "form": `
    + {{- form_error .Form nil -}} + + {{- form_help .Form -}} + + {{- range $field := .Form.Fields -}} + {{- form_row $field -}} + {{- end -}} +
    `, + "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`, + "help": ` + {{- if gt (len .Help) 0 -}} +
    {{ .Help }}
    + {{- end -}} + `, + "label": ` + {{ if .Field.HasOption "label" }} + {{ $label := (.Field.GetOption "label").Value }} + + {{- if ne $label "" -}} + + {{- end -}} + {{- end -}} + `, + "input": ` + {{- $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 -}} + {{- $class := "form-control" }} + + {{- if eq $type.Value "checkbox" -}} + {{- $value = 1 -}} + {{- end -}} + + {{- if or (eq $type.Value "checkbox") (eq $type.Value "radio") -}} + {{- $class = "form-check-input" -}} + {{- end -}} + + {{- if eq $type.Value "range" -}} + {{- $class = "form-range" -}} + {{- end -}} + + {{- if or (eq $type.Value "submit") (eq $type.Value "reset") (eq $type.Value "button") -}} + {{- $class = "" -}} + + {{ if .Field.HasOption "attr" }} + {{ $class = (.Field.GetOption "attr").Value.attr.class }} + {{ end }} + {{- end -}} + + + `, + "textarea": ` + + `, + "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 -}} + + {{- if and (not $required) (not $isMultiple) -}} + {{- $keyAdd = 1 -}} + {{- end -}} + + {{- if $isExpanded -}} + {{- if and (not $required) (not $isMultiple) -}} +
    + + +
    + {{- end -}} + + {{- range $key, $choice := $choices.GetChoices -}} +
    + + +
    + {{- end -}} + {{- else -}} + + {{- end -}} + `, + "sub_form": ` +
    + {{ if .Field.HasOption "label" }} + {{ $label := (.Field.GetOption "label").Value }} + + {{- if ne $label "" -}} + {{ $label }} + {{- end -}} + {{- end -}} + + {{ form_widget_help .Field }} + + {{- range $field := .Field.Children -}} + {{- form_row $field -}} + {{- end -}} +
    + `, + "error": ` + {{- if gt (len .Errors) 0 -}} +
    + {{- range $error := .Errors -}} +
    {{- $error -}}
    + {{- end -}} +
    + {{- end -}} + `, + "row": `
    + {{ $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_widget .Field -}} + {{- form_error nil .Field -}} + + {{ if and (eq (len .Field.Children) 0) ($labelAfterWidget) }} + {{- form_label .Field -}} + {{ end }} + + {{- form_widget_help .Field -}} +
    `, +} From f533cd60d5d39a450376d6b9746ff62d085a88d0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Jul 2025 15:22:42 +0200 Subject: [PATCH 008/117] feat: improve templates --- theme/renderer.go | 13 +++++++++++++ validation/mail.go | 9 ++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/theme/renderer.go b/theme/renderer.go index d43edc7..7037965 100644 --- a/theme/renderer.go +++ b/theme/renderer.go @@ -96,6 +96,18 @@ func (r *Renderer) RenderFormAttr(form *form.Form) template.HTMLAttr { }) } +func (r *Renderer) RenderRowAttr(field *form.Field) template.HTMLAttr { + var attributes map[string]string + + if field.HasOption("row_attr") { + attributes = field.GetOption("row_attr").Value.(map[string]string) + } + + return r.RenderAttr("raw_attr", r.Theme["attributes"], map[string]any{ + "Attributes": attributes, + }) +} + func (r *Renderer) RenderFormHelp(form *form.Form) template.HTML { var help string @@ -147,6 +159,7 @@ func (r *Renderer) FuncMap() template.FuncMap { "form_attr": r.RenderFormAttr, "form_widget_attr": r.RenderWidgetAttr, "form_label_attr": r.RenderLabelAttr, + "form_row_attr": r.RenderRowAttr, "form_help": r.RenderFormHelp, "form_widget_help": r.RenderWidgetHelp, "sum": func(values ...any) float64 { diff --git a/validation/mail.go b/validation/mail.go index d999dc7..ddc4504 100644 --- a/validation/mail.go +++ b/validation/mail.go @@ -3,6 +3,13 @@ package validation import "net/mail" type Mail struct { + Message string +} + +func NewMail() Mail { + return Mail{ + Message: "This value is not a valid email address.", + } } func (c Mail) Validate(data any) []Error { @@ -18,7 +25,7 @@ func (c Mail) Validate(data any) []Error { _, err := mail.ParseAddress(data.(string)) if err != nil { - errors = append(errors, Error("This value is not a valid email address.")) + errors = append(errors, Error(c.Message)) } return errors From 72be88a928ca55e8d2cf4d6ee850d0d6272a7af9 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Jul 2025 15:22:49 +0200 Subject: [PATCH 009/117] feat: improve templates --- theme/html5.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/theme/html5.go b/theme/html5.go index 2e1055a..0ea3d62 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -56,7 +56,7 @@ var Html5 = map[string]string{ {{- if $isExpanded -}} {{- if and (not $required) (not $isMultiple) -}} - + {{- end -}} {{- range $key, $choice := $choices.GetChoices -}} @@ -65,7 +65,7 @@ var Html5 = map[string]string{ {{- end -}} {{- else -}} + + CTRL K + + + +
    +
      +
      + + + + + +GitHub + + + + +
      +
      + + + + + +
      +
      +
      +

      Categories

      +
      +
      + +
      +
      + +
      +
      +
      +
      + +
      +

      + + + + + + diff --git a/categories/index.xml b/categories/index.xml new file mode 100644 index 0000000..fb03ab1 --- /dev/null +++ b/categories/index.xml @@ -0,0 +1,18 @@ + + + deblan/go-form – Categories + /categories/ + Recent content in Categories on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + diff --git a/css/compiled/main.css b/css/compiled/main.css new file mode 100644 index 0000000..c482e48 --- /dev/null +++ b/css/compiled/main.css @@ -0,0 +1,3637 @@ +/* +! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com +*//* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; /* 1 */ + border-width: 0; /* 2 */ + border-style: solid; /* 2 */ + border-color: #e5e7eb; /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -moz-tab-size: 4; /* 3 */ + -o-tab-size: 4; + tab-size: 4; /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ + font-feature-settings: normal; /* 5 */ + font-variation-settings: normal; /* 6 */ + -webkit-tap-highlight-color: transparent; /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; /* 1 */ + line-height: inherit; /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ + font-feature-settings: normal; /* 2 */ + font-variation-settings: normal; /* 3 */ + font-size: 1em; /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + font-size: 100%; /* 1 */ + font-weight: inherit; /* 1 */ + line-height: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; /* 1 */ + background-color: transparent; /* 2 */ + background-image: none; /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; /* 1 */ + color: #9ca3af; /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; /* 1 */ + color: #9ca3af; /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} +.hx-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} +.hx-pointer-events-none { + pointer-events: none; +} +.hx-fixed { + position: fixed; +} +.hx-absolute { + position: absolute; +} +.hx-relative { + position: relative; +} +.hx-sticky { + position: sticky; +} +.hx-inset-0 { + inset: 0px; +} +.hx-inset-x-0 { + left: 0px; + right: 0px; +} +.hx-inset-y-0 { + top: 0px; + bottom: 0px; +} +.hx-bottom-0 { + bottom: 0px; +} +.hx-left-\[24px\] { + left: 24px; +} +.hx-left-\[36px\] { + left: 36px; +} +.hx-right-0 { + right: 0px; +} +.hx-top-0 { + top: 0px; +} +.hx-top-16 { + top: 4rem; +} +.hx-top-8 { + top: 2rem; +} +.hx-top-\[40\%\] { + top: 40%; +} +.hx-top-full { + top: 100%; +} +.hx-z-10 { + z-index: 10; +} +.hx-z-20 { + z-index: 20; +} +.hx-z-\[-1\] { + z-index: -1; +} +.hx-order-last { + order: 9999; +} +.hx-m-\[11px\] { + margin: 11px; +} +.hx-mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} +.hx-mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} +.hx-mx-auto { + margin-left: auto; + margin-right: auto; +} +.hx-my-1\.5 { + margin-top: 0.375rem; + margin-bottom: 0.375rem; +} +.hx-my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} +.-hx-mb-0\.5 { + margin-bottom: -0.125rem; +} +.-hx-ml-2 { + margin-left: -0.5rem; +} +.-hx-mr-2 { + margin-right: -0.5rem; +} +.-hx-mt-20 { + margin-top: -5rem; +} +.hx-mb-10 { + margin-bottom: 2.5rem; +} +.hx-mb-12 { + margin-bottom: 3rem; +} +.hx-mb-16 { + margin-bottom: 4rem; +} +.hx-mb-2 { + margin-bottom: 0.5rem; +} +.hx-mb-4 { + margin-bottom: 1rem; +} +.hx-mb-6 { + margin-bottom: 1.5rem; +} +.hx-mb-8 { + margin-bottom: 2rem; +} +.hx-ml-4 { + margin-left: 1rem; +} +.hx-mr-1 { + margin-right: 0.25rem; +} +.hx-mr-2 { + margin-right: 0.5rem; +} +.hx-mt-1 { + margin-top: 0.25rem; +} +.hx-mt-1\.5 { + margin-top: 0.375rem; +} +.hx-mt-12 { + margin-top: 3rem; +} +.hx-mt-16 { + margin-top: 4rem; +} +.hx-mt-2 { + margin-top: 0.5rem; +} +.hx-mt-4 { + margin-top: 1rem; +} +.hx-mt-5 { + margin-top: 1.25rem; +} +.hx-mt-6 { + margin-top: 1.5rem; +} +.hx-mt-8 { + margin-top: 2rem; +} +.hx-line-clamp-3 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; +} +.hx-block { + display: block; +} +.hx-inline-block { + display: inline-block; +} +.hx-inline { + display: inline; +} +.hx-flex { + display: flex; +} +.hx-inline-flex { + display: inline-flex; +} +.hx-grid { + display: grid; +} +.hx-hidden { + display: none; +} +.hx-aspect-auto { + aspect-ratio: auto; +} +.hx-h-0 { + height: 0px; +} +.hx-h-16 { + height: 4rem; +} +.hx-h-2 { + height: 0.5rem; +} +.hx-h-3\.5 { + height: 0.875rem; +} +.hx-h-4 { + height: 1rem; +} +.hx-h-5 { + height: 1.25rem; +} +.hx-h-7 { + height: 1.75rem; +} +.hx-h-\[18px\] { + height: 18px; +} +.hx-h-full { + height: 100%; +} +.hx-max-h-64 { + max-height: 16rem; +} +.hx-max-h-\[calc\(100vh-var\(--navbar-height\)-env\(safe-area-inset-bottom\)\)\] { + max-height: calc(100vh - var(--navbar-height) - env(safe-area-inset-bottom)); +} +.hx-max-h-\[min\(calc\(50vh-11rem-env\(safe-area-inset-bottom\)\)\,400px\)\] { + max-height: min(calc(50vh - 11rem - env(safe-area-inset-bottom)),400px); +} +.hx-min-h-\[100px\] { + min-height: 100px; +} +.hx-min-h-\[calc\(100vh-var\(--navbar-height\)\)\] { + min-height: calc(100vh - var(--navbar-height)); +} +.hx-w-2 { + width: 0.5rem; +} +.hx-w-3\.5 { + width: 0.875rem; +} +.hx-w-4 { + width: 1rem; +} +.hx-w-64 { + width: 16rem; +} +.hx-w-\[110\%\] { + width: 110%; +} +.hx-w-\[180\%\] { + width: 180%; +} +.hx-w-full { + width: 100%; +} +.hx-w-max { + width: -moz-max-content; + width: max-content; +} +.hx-w-screen { + width: 100vw; +} +.hx-min-w-0 { + min-width: 0px; +} +.hx-min-w-\[18px\] { + min-width: 18px; +} +.hx-min-w-\[24px\] { + min-width: 24px; +} +.hx-min-w-full { + min-width: 100%; +} +.hx-max-w-6xl { + max-width: 72rem; +} +.hx-max-w-\[50\%\] { + max-width: 50%; +} +.hx-max-w-\[90rem\] { + max-width: 90rem; +} +.hx-max-w-\[min\(calc\(100vw-2rem\)\,calc\(100\%\+20rem\)\)\] { + max-width: min(calc(100vw - 2rem),calc(100% + 20rem)); +} +.hx-max-w-none { + max-width: none; +} +.hx-max-w-screen-xl { + max-width: 1280px; +} +.hx-shrink-0 { + flex-shrink: 0; +} +.hx-grow { + flex-grow: 1; +} +.hx-origin-center { + transform-origin: center; +} +.hx-cursor-default { + cursor: default; +} +.hx-cursor-pointer { + cursor: pointer; +} +.hx-select-none { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} +.hx-scroll-my-6 { + scroll-margin-top: 1.5rem; + scroll-margin-bottom: 1.5rem; +} +.hx-scroll-py-6 { + scroll-padding-top: 1.5rem; + scroll-padding-bottom: 1.5rem; +} +.hx-list-none { + list-style-type: none; +} +.hx-appearance-none { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +.hx-grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} +.hx-flex-col { + flex-direction: column; +} +.hx-flex-wrap { + flex-wrap: wrap; +} +.hx-items-start { + align-items: flex-start; +} +.hx-items-center { + align-items: center; +} +.hx-justify-start { + justify-content: flex-start; +} +.hx-justify-end { + justify-content: flex-end; +} +.hx-justify-center { + justify-content: center; +} +.hx-justify-between { + justify-content: space-between; +} +.hx-justify-items-start { + justify-items: start; +} +.hx-gap-1 { + gap: 0.25rem; +} +.hx-gap-2 { + gap: 0.5rem; +} +.hx-gap-4 { + gap: 1rem; +} +.hx-gap-x-1\.5 { + -moz-column-gap: 0.375rem; + column-gap: 0.375rem; +} +.hx-gap-y-2 { + row-gap: 0.5rem; +} +.hx-overflow-auto { + overflow: auto; +} +.hx-overflow-hidden { + overflow: hidden; +} +.hx-overflow-x-auto { + overflow-x: auto; +} +.hx-overflow-y-auto { + overflow-y: auto; +} +.hx-overflow-x-hidden { + overflow-x: hidden; +} +.hx-overflow-y-hidden { + overflow-y: hidden; +} +.hx-overscroll-contain { + overscroll-behavior: contain; +} +.hx-overscroll-x-contain { + overscroll-behavior-x: contain; +} +.hx-text-ellipsis { + text-overflow: ellipsis; +} +.hx-whitespace-nowrap { + white-space: nowrap; +} +.hx-break-words { + overflow-wrap: break-word; +} +.hx-rounded { + border-radius: 0.25rem; +} +.hx-rounded-3xl { + border-radius: 1.5rem; +} +.hx-rounded-full { + border-radius: 9999px; +} +.hx-rounded-lg { + border-radius: 0.5rem; +} +.hx-rounded-md { + border-radius: 0.375rem; +} +.hx-rounded-sm { + border-radius: 0.125rem; +} +.hx-rounded-xl { + border-radius: 0.75rem; +} +.hx-rounded-t { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} +.hx-border { + border-width: 1px; +} +.hx-border-b { + border-bottom-width: 1px; +} +.hx-border-b-2 { + border-bottom-width: 2px; +} +.hx-border-t { + border-top-width: 1px; +} +.hx-border-amber-200 { + --tw-border-opacity: 1; + border-color: rgb(253 230 138 / var(--tw-border-opacity)); +} +.hx-border-black\/5 { + border-color: rgb(0 0 0 / 0.05); +} +.hx-border-blue-200 { + --tw-border-opacity: 1; + border-color: rgb(191 219 254 / var(--tw-border-opacity)); +} +.hx-border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); +} +.hx-border-gray-500 { + --tw-border-opacity: 1; + border-color: rgb(107 114 128 / var(--tw-border-opacity)); +} +.hx-border-green-200 { + --tw-border-opacity: 1; + border-color: rgb(187 247 208 / var(--tw-border-opacity)); +} +.hx-border-indigo-200 { + --tw-border-opacity: 1; + border-color: rgb(199 210 254 / var(--tw-border-opacity)); +} +.hx-border-orange-100 { + --tw-border-opacity: 1; + border-color: rgb(255 237 213 / var(--tw-border-opacity)); +} +.hx-border-red-200 { + --tw-border-opacity: 1; + border-color: rgb(254 202 202 / var(--tw-border-opacity)); +} +.hx-border-transparent { + border-color: transparent; +} +.hx-border-yellow-100 { + --tw-border-opacity: 1; + border-color: rgb(254 249 195 / var(--tw-border-opacity)); +} +.hx-bg-amber-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 243 199 / var(--tw-bg-opacity)); +} +.hx-bg-black\/80 { + background-color: rgb(0 0 0 / 0.8); +} +.hx-bg-black\/\[\.05\] { + background-color: rgb(0 0 0 / .05); +} +.hx-bg-blue-100 { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} +.hx-bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} +.hx-bg-green-100 { + --tw-bg-opacity: 1; + background-color: rgb(220 252 231 / var(--tw-bg-opacity)); +} +.hx-bg-indigo-100 { + --tw-bg-opacity: 1; + background-color: rgb(224 231 255 / var(--tw-bg-opacity)); +} +.hx-bg-neutral-50 { + --tw-bg-opacity: 1; + background-color: rgb(250 250 250 / var(--tw-bg-opacity)); +} +.hx-bg-orange-50 { + --tw-bg-opacity: 1; + background-color: rgb(255 247 237 / var(--tw-bg-opacity)); +} +.hx-bg-primary-100 { + --tw-bg-opacity: 1; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 44) / var(--tw-bg-opacity)); +} +.hx-bg-primary-400 { + --tw-bg-opacity: 1; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 16) / var(--tw-bg-opacity)); +} +.hx-bg-primary-600 { + --tw-bg-opacity: 1; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-bg-opacity)); +} +.hx-bg-primary-700\/5 { + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 39) / 0.05); +} +.hx-bg-red-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 226 226 / var(--tw-bg-opacity)); +} +.hx-bg-transparent { + background-color: transparent; +} +.hx-bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} +.hx-bg-yellow-50 { + --tw-bg-opacity: 1; + background-color: rgb(254 252 232 / var(--tw-bg-opacity)); +} +.hx-bg-gradient-to-r { + background-image: linear-gradient(to right, var(--tw-gradient-stops)); +} +.hx-from-gray-900 { + --tw-gradient-from: #111827 var(--tw-gradient-from-position); + --tw-gradient-to: rgb(17 24 39 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} +.hx-to-gray-600 { + --tw-gradient-to: #4b5563 var(--tw-gradient-to-position); +} +.hx-bg-clip-text { + -webkit-background-clip: text; + background-clip: text; +} +.hx-p-0\.5 { + padding: 0.125rem; +} +.hx-p-1 { + padding: 0.25rem; +} +.hx-p-1\.5 { + padding: 0.375rem; +} +.hx-p-2 { + padding: 0.5rem; +} +.hx-p-4 { + padding: 1rem; +} +.hx-p-6 { + padding: 1.5rem; +} +.hx-px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; +} +.hx-px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} +.hx-px-2\.5 { + padding-left: 0.625rem; + padding-right: 0.625rem; +} +.hx-px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} +.hx-px-4 { + padding-left: 1rem; + padding-right: 1rem; +} +.hx-px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} +.hx-py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} +.hx-py-1\.5 { + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} +.hx-py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} +.hx-py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.hx-py-2\.5 { + padding-top: 0.625rem; + padding-bottom: 0.625rem; +} +.hx-py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} +.hx-py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} +.hx-pb-8 { + padding-bottom: 2rem; +} +.hx-pb-\[env\(safe-area-inset-bottom\)\] { + padding-bottom: env(safe-area-inset-bottom); +} +.hx-pb-px { + padding-bottom: 1px; +} +.hx-pl-\[max\(env\(safe-area-inset-left\)\,1\.5rem\)\] { + padding-left: max(env(safe-area-inset-left),1.5rem); +} +.hx-pr-2 { + padding-right: 0.5rem; +} +.hx-pr-4 { + padding-right: 1rem; +} +.hx-pr-\[calc\(env\(safe-area-inset-right\)-1\.5rem\)\] { + padding-right: calc(env(safe-area-inset-right) - 1.5rem); +} +.hx-pr-\[max\(env\(safe-area-inset-left\)\,1\.5rem\)\] { + padding-right: max(env(safe-area-inset-left),1.5rem); +} +.hx-pr-\[max\(env\(safe-area-inset-right\)\,1\.5rem\)\] { + padding-right: max(env(safe-area-inset-right),1.5rem); +} +.hx-pt-4 { + padding-top: 1rem; +} +.hx-pt-6 { + padding-top: 1.5rem; +} +.hx-pt-8 { + padding-top: 2rem; +} +.hx-text-left { + text-align: left; +} +.hx-text-center { + text-align: center; +} +.hx-align-middle { + vertical-align: middle; +} +.hx-align-text-bottom { + vertical-align: text-bottom; +} +.hx-align-\[-2\.5px\] { + vertical-align: -2.5px; +} +.hx-font-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} +.hx-text-2xl { + font-size: 1.5rem; +} +.hx-text-4xl { + font-size: 2.25rem; +} +.hx-text-\[\.65rem\] { + font-size: .65rem; +} +.hx-text-\[10px\] { + font-size: 10px; +} +.hx-text-base { + font-size: 1rem; +} +.hx-text-lg { + font-size: 1.125rem; +} +.hx-text-sm { + font-size: .875rem; +} +.hx-text-xl { + font-size: 1.25rem; +} +.hx-text-xs { + font-size: .75rem; +} +.hx-font-bold { + font-weight: 700; +} +.hx-font-extrabold { + font-weight: 800; +} +.hx-font-medium { + font-weight: 500; +} +.hx-font-normal { + font-weight: 400; +} +.hx-font-semibold { + font-weight: 600; +} +.hx-capitalize { + text-transform: capitalize; +} +.hx-leading-5 { + line-height: 1.25rem; +} +.hx-leading-6 { + line-height: 1.5rem; +} +.hx-leading-7 { + line-height: 1.75rem; +} +.hx-leading-none { + line-height: 1; +} +.hx-leading-tight { + line-height: 1.25; +} +.hx-tracking-tight { + letter-spacing: -0.015em; +} +.hx-text-\[color\:hsl\(var\(--primary-hue\)\,100\%\,50\%\)\] { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) 100% 50% / var(--tw-text-opacity)); +} +.hx-text-amber-900 { + --tw-text-opacity: 1; + color: rgb(120 53 15 / var(--tw-text-opacity)); +} +.hx-text-blue-900 { + --tw-text-opacity: 1; + color: rgb(30 58 138 / var(--tw-text-opacity)); +} +.hx-text-current { + color: currentColor; +} +.hx-text-gray-100 { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); +} +.hx-text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} +.hx-text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} +.hx-text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} +.hx-text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} +.hx-text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} +.hx-text-green-900 { + --tw-text-opacity: 1; + color: rgb(20 83 45 / var(--tw-text-opacity)); +} +.hx-text-indigo-900 { + --tw-text-opacity: 1; + color: rgb(49 46 129 / var(--tw-text-opacity)); +} +.hx-text-orange-800 { + --tw-text-opacity: 1; + color: rgb(154 52 18 / var(--tw-text-opacity)); +} +.hx-text-primary-800 { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 32) / var(--tw-text-opacity)); +} +.hx-text-red-900 { + --tw-text-opacity: 1; + color: rgb(127 29 29 / var(--tw-text-opacity)); +} +.hx-text-slate-900 { + --tw-text-opacity: 1; + color: rgb(15 23 42 / var(--tw-text-opacity)); +} +.hx-text-transparent { + color: transparent; +} +.hx-text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} +.hx-text-yellow-900 { + --tw-text-opacity: 1; + color: rgb(113 63 18 / var(--tw-text-opacity)); +} +.hx-underline { + text-decoration-line: underline; +} +.hx-no-underline { + text-decoration-line: none; +} +.hx-decoration-from-font { + text-decoration-thickness: from-font; +} +.hx-underline-offset-2 { + text-underline-offset: 2px; +} +.hx-opacity-0 { + opacity: 0; +} +.hx-opacity-50 { + opacity: 0.5; +} +.hx-opacity-80 { + opacity: 0.8; +} +.hx-shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hx-shadow-\[0_-12px_16px_\#fff\] { + --tw-shadow: 0 -12px 16px #fff; + --tw-shadow-colored: 0 -12px 16px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hx-shadow-\[0_-12px_16px_white\] { + --tw-shadow: 0 -12px 16px white; + --tw-shadow-colored: 0 -12px 16px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hx-shadow-\[0_2px_4px_rgba\(0\,0\,0\,\.02\)\,0_1px_0_rgba\(0\,0\,0\,\.06\)\] { + --tw-shadow: 0 2px 4px rgba(0,0,0,.02),0 1px 0 rgba(0,0,0,.06); + --tw-shadow-colored: 0 2px 4px var(--tw-shadow-color), 0 1px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hx-shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hx-shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hx-shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hx-shadow-gray-100 { + --tw-shadow-color: #f3f4f6; + --tw-shadow: var(--tw-shadow-colored); +} +.hx-ring-1 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} +.hx-ring-black\/5 { + --tw-ring-color: rgb(0 0 0 / 0.05); +} +.hx-transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.hx-transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.hx-transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.hx-transition-opacity { + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.hx-transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.hx-duration-200 { + transition-duration: 200ms; +} +.hx-duration-75 { + transition-duration: 75ms; +} +.hx-ease-in { + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); +} +.\[-webkit-tap-highlight-color\:transparent\] { + -webkit-tap-highlight-color: transparent; +} +.\[-webkit-touch-callout\:none\] { + -webkit-touch-callout: none; +} +.\[counter-reset\:step\] { + counter-reset: step; +} +.\[hyphens\:auto\] { + -webkit-hyphens: auto; + hyphens: auto; +} +.\[transition\:background-color_1\.5s_ease\] { + transition: background-color 1.5s ease; +} +.\[word-break\:break-word\] { + word-break: break-word; +} +.content :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 0.5rem; + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.015em; + --tw-text-opacity: 1; + color: rgb(15 23 42 / var(--tw-text-opacity)); +} +.content :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(241 245 249 / var(--tw-text-opacity)); +} +.content :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 2.5rem; + border-bottom-width: 1px; + border-color: rgb(229 229 229 / 0.7); + padding-bottom: 0.25rem; + font-size: 1.875rem; + font-weight: 600; + letter-spacing: -0.015em; + --tw-text-opacity: 1; + color: rgb(15 23 42 / var(--tw-text-opacity)); +} +@media (prefers-contrast: more) { + + .content :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)) { + --tw-border-opacity: 1; + border-color: rgb(163 163 163 / var(--tw-border-opacity)); + } +} +.content :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 44) / 0.1); + --tw-text-opacity: 1; + color: rgb(241 245 249 / var(--tw-text-opacity)); +} +@media (prefers-contrast: more) { + + .content :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(163 163 163 / var(--tw-border-opacity)); + } +} +.content :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 2rem; + font-size: 1.5rem; + font-weight: 600; + letter-spacing: -0.015em; + --tw-text-opacity: 1; + color: rgb(15 23 42 / var(--tw-text-opacity)); +} +.content :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(241 245 249 / var(--tw-text-opacity)); +} +.content :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 2rem; + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.015em; + --tw-text-opacity: 1; + color: rgb(15 23 42 / var(--tw-text-opacity)); +} +.content :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(241 245 249 / var(--tw-text-opacity)); +} +.content :where(h5):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 2rem; + font-size: 1.125rem; + font-weight: 600; + letter-spacing: -0.015em; + --tw-text-opacity: 1; + color: rgb(15 23 42 / var(--tw-text-opacity)); +} +.content :where(h5):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(241 245 249 / var(--tw-text-opacity)); +} +.content :where(h6):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 2rem; + font-size: 1rem; + font-weight: 600; + letter-spacing: -0.015em; + --tw-text-opacity: 1; + color: rgb(15 23 42 / var(--tw-text-opacity)); +} +.content :where(h6):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(241 245 249 / var(--tw-text-opacity)); +} +.content :where(p):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 1.5rem; + line-height: 1.75rem; +} +.content :where(p):not(:where([class~=not-prose],[class~=not-prose] *)):first-child { + margin-top: 0px; +} +.content :where(a):not(:where([class~=not-prose],[class~=not-prose] *)) { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-text-opacity)); + text-decoration-line: underline; + text-decoration-thickness: from-font; + text-underline-position: from-font; +} +.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 1.5rem; + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); + font-style: italic; + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} +.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)):first-child { + margin-top: 0px; +} +.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-border-opacity)); + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir="ltr"], [dir="ltr"] *) { + border-left-width: 2px; + padding-left: 1.5rem; +} +.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir="rtl"], [dir="rtl"] *) { + border-right-width: 2px; + padding-right: 1.5rem; +} +.content :where(pre):not(:where(.hextra-code-block pre, [class~=not-prose],[class~=not-prose] *)) { + margin-bottom: 1rem; + overflow-x: auto; + border-radius: 0.75rem; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 39) / 0.05); + padding-top: 1rem; + padding-bottom: 1rem; + font-size: .9em; + font-weight: 500; + -webkit-font-smoothing: auto; + -moz-osx-font-smoothing: auto; +} +@media (prefers-contrast: more) { + + .content :where(pre):not(:where(.hextra-code-block pre, [class~=not-prose],[class~=not-prose] *)) { + border-width: 1px; + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 24) / 0.2); + --tw-contrast: contrast(1.5); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); + } +} +.content :where(pre):not(:where(.hextra-code-block pre, [class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 27) / 0.1); +} +@media (prefers-contrast: more) { + + .content :where(pre):not(:where(.hextra-code-block pre, [class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 44) / 0.4); + } +} +.content :where(code):not(:where(.hextra-code-block code, [class~=not-prose],[class~=not-prose] *)) { + overflow-wrap: break-word; + border-radius: 0.375rem; + border-width: 1px; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); + --tw-border-opacity: 0.04; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); + --tw-bg-opacity: 0.03; + padding-top: 0.125rem; + padding-bottom: 0.125rem; + padding-left: .25em; + padding-right: .25em; + font-size: .9em; +} +.content :where(code):not(:where(.hextra-code-block code, [class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(255 255 255 / 0.1); +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) { + margin-top: 1.5rem; + display: block; + overflow-x: auto; + padding: 0px; +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)):first-child { + margin-top: 0px; +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) tr { + margin: 0px; + border-top-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); + padding: 0px; +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) tr:nth-child(even) { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) tr:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) tr:is(html[class~="dark"] *):nth-child(even) { + background-color: rgb(75 85 99 / 0.2); +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) th { + margin: 0px; + border-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-weight: 600; +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) th:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) td { + margin: 0px; + border-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.content :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) td:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); +} +.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 1.5rem; + list-style-type: decimal; +} +.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)):first-child { + margin-top: 0px; +} +.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir="ltr"], [dir="ltr"] *) { + margin-left: 1.5rem; +} +.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir="rtl"], [dir="rtl"] *) { + margin-right: 1.5rem; +} +.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)) li { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} +.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 1.5rem; + list-style-type: disc; +} +.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)):first-child { + margin-top: 0px; +} +.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir="ltr"], [dir="ltr"] *) { + margin-left: 1.5rem; +} +.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir="rtl"], [dir="rtl"] *) { + margin-right: 1.5rem; +} +.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)) li { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} +/* This CSS rule targets the first nested unordered (ul) or ordered (ol) list + inside the list item (li) of any parent ul or ol. + The rule sets the top margin of the selected list to zero. */ +.content :where(ul, ol) > li > :where(ul, ol):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-top: 0px; +} +.content :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)) { + overflow-wrap: break-word; + border-radius: 0.375rem; + border-width: 1px; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); + --tw-border-opacity: 0.04; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); + --tw-bg-opacity: 0.03; + padding-top: 0.125rem; + padding-bottom: 0.125rem; + padding-left: .25em; + padding-right: .25em; + font-size: .9em; +} +.content :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(255 255 255 / 0.1); +} +.content :where(pre.mermaid):not(:where(.hextra-code-block pre, [class~=not-prose],[class~=not-prose] *)) { + border-radius: 0px; + background-color: transparent; +} +.content :where(pre.mermaid):not(:where(.hextra-code-block pre, [class~=not-prose],[class~=not-prose] *)):is(html[class~="dark"] *) { + background-color: transparent; +} +.content :where(img):not(:where([class~=not-prose],[class~=not-prose] *)) { + margin-left: auto; + margin-right: auto; + margin-top: 1rem; + margin-bottom: 1rem; + border-radius: 0.375rem; +} +.content :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)) figcaption { + margin-top: 0.5rem; + display: block; + text-align: center; + font-size: .875rem; + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} +.content :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)) figcaption:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +/* Definition list */ +.content :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)) dt { + margin-top: 1.5rem; + font-weight: 600; +} +.content :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)) dd { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + padding-inline-start: 1.5rem; +} +.content .footnotes { + margin-top: 3rem; + font-size: .875rem; +} +.subheading-anchor { + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.subheading-anchor:where([dir="ltr"], [dir="ltr"] *) { + margin-left: 0.25rem; +} +.subheading-anchor:where([dir="rtl"], [dir="rtl"] *) { + margin-right: 0.25rem; +} +span:target + .subheading-anchor, + :hover > .subheading-anchor, + .subheading-anchor:focus { + opacity: 1; +} +span + .subheading-anchor, + :hover > .subheading-anchor { + text-decoration-line: none !important; +} +.subheading-anchor:after { + padding-left: 0.25rem; + padding-right: 0.25rem; + --tw-content: '#'; + content: var(--tw-content); + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} +.subheading-anchor:is(html[class~="dark"] *):after { + --tw-text-opacity: 1; + color: rgb(64 64 64 / var(--tw-text-opacity)); +} +span:target + .subheading-anchor:after { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +span:target + .subheading-anchor:is(html[class~="dark"] *):after { + --tw-text-opacity: 1; + color: rgb(115 115 115 / var(--tw-text-opacity)); +} +article details > summary::-webkit-details-marker { + display: none; +} +article details > summary::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='hx-h-5 hx-w-5' viewBox='0 0 20 20' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd' /%3E%3C/svg%3E"); + height: 1.2em; + width: 1.2em; + vertical-align: -4px; + padding: 0 0.6em; + } +:lang(fa) ol { + list-style-type: persian; +} +/* Code syntax highlight */ +/* Light theme for syntax highlight */ +/* Generated using `hugo gen chromastyles --style=github` */ +.highlight { + /* Background .bg { background-color: #ffffff; } */ + /* PreWrapper .chroma { background-color: #ffffff; } */ + /* Other .chroma .x { } */ + /* CodeLine .chroma .cl { } */ + /* LineTableTD .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } */ + /* LineTable .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } */ + /* LineHighlight .chroma .hl { background-color: #ffffcc } */ + /* LineNumbersTable .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } */ + /* LineNumbers .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } */ + /* Name .chroma .n { } */ + /* NameFunctionMagic .chroma .fm { } */ + /* NameOther .chroma .nx { } */ + /* NameProperty .chroma .py { } */ + /* NameVariableMagic .chroma .vm { } */ + /* Literal .chroma .l { } */ + /* LiteralDate .chroma .ld { } */ + /* Punctuation .chroma .p { } */ + /* Generic .chroma .g { } */ +} +/* Error */ +.highlight .chroma .err { color: #a61717; background-color: #e3d2d2 } +/* LineLink */ +.highlight .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* Line */ +.highlight .chroma .line { display: flex; } +/* Keyword */ +.highlight .chroma .k { color: #000000; font-weight: bold } +/* KeywordConstant */ +.highlight .chroma .kc { color: #000000; font-weight: bold } +/* KeywordDeclaration */ +.highlight .chroma .kd { color: #000000; font-weight: bold } +/* KeywordNamespace */ +.highlight .chroma .kn { color: #000000; font-weight: bold } +/* KeywordPseudo */ +.highlight .chroma .kp { color: #000000; font-weight: bold } +/* KeywordReserved */ +.highlight .chroma .kr { color: #000000; font-weight: bold } +/* KeywordType */ +.highlight .chroma .kt { color: #445588; font-weight: bold } +/* NameAttribute */ +.highlight .chroma .na { color: #008080 } +/* NameBuiltin */ +.highlight .chroma .nb { color: #0086b3 } +/* NameBuiltinPseudo */ +.highlight .chroma .bp { color: #999999 } +/* NameClass */ +.highlight .chroma .nc { color: #445588; font-weight: bold } +/* NameConstant */ +.highlight .chroma .no { color: #008080 } +/* NameDecorator */ +.highlight .chroma .nd { color: #3c5d5d; font-weight: bold } +/* NameEntity */ +.highlight .chroma .ni { color: #800080 } +/* NameException */ +.highlight .chroma .ne { color: #990000; font-weight: bold } +/* NameFunction */ +.highlight .chroma .nf { color: #990000; font-weight: bold } +/* NameLabel */ +.highlight .chroma .nl { color: #990000; font-weight: bold } +/* NameNamespace */ +.highlight .chroma .nn { color: #555555 } +/* NameTag */ +.highlight .chroma .nt { color: #000080 } +/* NameVariable */ +.highlight .chroma .nv { color: #008080 } +/* NameVariableClass */ +.highlight .chroma .vc { color: #008080 } +/* NameVariableGlobal */ +.highlight .chroma .vg { color: #008080 } +/* NameVariableInstance */ +.highlight .chroma .vi { color: #008080 } +/* LiteralString */ +.highlight .chroma .s { color: #dd1144 } +/* LiteralStringAffix */ +.highlight .chroma .sa { color: #dd1144 } +/* LiteralStringBacktick */ +.highlight .chroma .sb { color: #dd1144 } +/* LiteralStringChar */ +.highlight .chroma .sc { color: #dd1144 } +/* LiteralStringDelimiter */ +.highlight .chroma .dl { color: #dd1144 } +/* LiteralStringDoc */ +.highlight .chroma .sd { color: #dd1144 } +/* LiteralStringDouble */ +.highlight .chroma .s2 { color: #dd1144 } +/* LiteralStringEscape */ +.highlight .chroma .se { color: #dd1144 } +/* LiteralStringHeredoc */ +.highlight .chroma .sh { color: #dd1144 } +/* LiteralStringInterpol */ +.highlight .chroma .si { color: #dd1144 } +/* LiteralStringOther */ +.highlight .chroma .sx { color: #dd1144 } +/* LiteralStringRegex */ +.highlight .chroma .sr { color: #009926 } +/* LiteralStringSingle */ +.highlight .chroma .s1 { color: #dd1144 } +/* LiteralStringSymbol */ +.highlight .chroma .ss { color: #990073 } +/* LiteralNumber */ +.highlight .chroma .m { color: #009999 } +/* LiteralNumberBin */ +.highlight .chroma .mb { color: #009999 } +/* LiteralNumberFloat */ +.highlight .chroma .mf { color: #009999 } +/* LiteralNumberHex */ +.highlight .chroma .mh { color: #009999 } +/* LiteralNumberInteger */ +.highlight .chroma .mi { color: #009999 } +/* LiteralNumberIntegerLong */ +.highlight .chroma .il { color: #009999 } +/* LiteralNumberOct */ +.highlight .chroma .mo { color: #009999 } +/* Operator */ +.highlight .chroma .o { color: #000000; font-weight: bold } +/* OperatorWord */ +.highlight .chroma .ow { color: #000000; font-weight: bold } +/* Comment */ +.highlight .chroma .c { color: #999988; font-style: italic } +/* CommentHashbang */ +.highlight .chroma .ch { color: #999988; font-style: italic } +/* CommentMultiline */ +.highlight .chroma .cm { color: #999988; font-style: italic } +/* CommentSingle */ +.highlight .chroma .c1 { color: #999988; font-style: italic } +/* CommentSpecial */ +.highlight .chroma .cs { color: #999999; font-weight: bold; font-style: italic } +/* CommentPreproc */ +.highlight .chroma .cp { color: #999999; font-weight: bold; font-style: italic } +/* CommentPreprocFile */ +.highlight .chroma .cpf { color: #999999; font-weight: bold; font-style: italic } +/* GenericDeleted */ +.highlight .chroma .gd { color: #000000; background-color: #ffdddd } +/* GenericEmph */ +.highlight .chroma .ge { color: #000000; font-style: italic } +/* GenericError */ +.highlight .chroma .gr { color: #aa0000 } +/* GenericHeading */ +.highlight .chroma .gh { color: #999999 } +/* GenericInserted */ +.highlight .chroma .gi { color: #000000; background-color: #ddffdd } +/* GenericOutput */ +.highlight .chroma .go { color: #888888 } +/* GenericPrompt */ +.highlight .chroma .gp { color: #555555 } +/* GenericStrong */ +.highlight .chroma .gs { font-weight: bold } +/* GenericSubheading */ +.highlight .chroma .gu { color: #aaaaaa } +/* GenericTraceback */ +.highlight .chroma .gt { color: #aa0000 } +/* GenericUnderline */ +.highlight .chroma .gl { text-decoration: underline } +/* TextWhitespace */ +.highlight .chroma .w { color: #bbbbbb } +.dark .highlight { + /* Background .bg { color: #c9d1d9; background-color: #0d1117; } + /* PreWrapper .chroma { color: #c9d1d9; background-color: #0d1117; } */ + /* LineTableTD .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } */ + /* LineTable .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } */ + /* LineHighlight .chroma .hl { background-color: #ffffcc } */ + /* LineNumbersTable .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #64686c } */ + /* LineNumbers .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #6e7681 } */ +} +/* Other */ +.dark .highlight .chroma .x { } +/* Error */ +.dark .highlight .chroma .err { color: #f85149 } +/* CodeLine */ +.dark .highlight .chroma .cl { } +/* LineLink */ +.dark .highlight .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* Line */ +.dark .highlight .chroma .line { display: flex; } +/* Keyword */ +.dark .highlight .chroma .k { color: #ff7b72 } +/* KeywordConstant */ +.dark .highlight .chroma .kc { color: #79c0ff } +/* KeywordDeclaration */ +.dark .highlight .chroma .kd { color: #ff7b72 } +/* KeywordNamespace */ +.dark .highlight .chroma .kn { color: #ff7b72 } +/* KeywordPseudo */ +.dark .highlight .chroma .kp { color: #79c0ff } +/* KeywordReserved */ +.dark .highlight .chroma .kr { color: #ff7b72 } +/* KeywordType */ +.dark .highlight .chroma .kt { color: #ff7b72 } +/* Name */ +.dark .highlight .chroma .n { } +/* NameAttribute */ +.dark .highlight .chroma .na { } +/* NameBuiltin */ +.dark .highlight .chroma .nb { } +/* NameBuiltinPseudo */ +.dark .highlight .chroma .bp { } +/* NameClass */ +.dark .highlight .chroma .nc { color: #f0883e; font-weight: bold } +/* NameConstant */ +.dark .highlight .chroma .no { color: #79c0ff; font-weight: bold } +/* NameDecorator */ +.dark .highlight .chroma .nd { color: #d2a8ff; font-weight: bold } +/* NameEntity */ +.dark .highlight .chroma .ni { color: #ffa657 } +/* NameException */ +.dark .highlight .chroma .ne { color: #f0883e; font-weight: bold } +/* NameFunction */ +.dark .highlight .chroma .nf { color: #d2a8ff; font-weight: bold } +/* NameFunctionMagic */ +.dark .highlight .chroma .fm { } +/* NameLabel */ +.dark .highlight .chroma .nl { color: #79c0ff; font-weight: bold } +/* NameNamespace */ +.dark .highlight .chroma .nn { color: #ff7b72 } +/* NameOther */ +.dark .highlight .chroma .nx { } +/* NameProperty */ +.dark .highlight .chroma .py { color: #79c0ff } +/* NameTag */ +.dark .highlight .chroma .nt { color: #7ee787 } +/* NameVariable */ +.dark .highlight .chroma .nv { color: #79c0ff } +/* NameVariableClass */ +.dark .highlight .chroma .vc { } +/* NameVariableGlobal */ +.dark .highlight .chroma .vg { } +/* NameVariableInstance */ +.dark .highlight .chroma .vi { } +/* NameVariableMagic */ +.dark .highlight .chroma .vm { } +/* Literal */ +.dark .highlight .chroma .l { color: #a5d6ff } +/* LiteralDate */ +.dark .highlight .chroma .ld { color: #79c0ff } +/* LiteralString */ +.dark .highlight .chroma .s { color: #a5d6ff } +/* LiteralStringAffix */ +.dark .highlight .chroma .sa { color: #79c0ff } +/* LiteralStringBacktick */ +.dark .highlight .chroma .sb { color: #a5d6ff } +/* LiteralStringChar */ +.dark .highlight .chroma .sc { color: #a5d6ff } +/* LiteralStringDelimiter */ +.dark .highlight .chroma .dl { color: #79c0ff } +/* LiteralStringDoc */ +.dark .highlight .chroma .sd { color: #a5d6ff } +/* LiteralStringDouble */ +.dark .highlight .chroma .s2 { color: #a5d6ff } +/* LiteralStringEscape */ +.dark .highlight .chroma .se { color: #79c0ff } +/* LiteralStringHeredoc */ +.dark .highlight .chroma .sh { color: #79c0ff } +/* LiteralStringInterpol */ +.dark .highlight .chroma .si { color: #a5d6ff } +/* LiteralStringOther */ +.dark .highlight .chroma .sx { color: #a5d6ff } +/* LiteralStringRegex */ +.dark .highlight .chroma .sr { color: #79c0ff } +/* LiteralStringSingle */ +.dark .highlight .chroma .s1 { color: #a5d6ff } +/* LiteralStringSymbol */ +.dark .highlight .chroma .ss { color: #a5d6ff } +/* LiteralNumber */ +.dark .highlight .chroma .m { color: #a5d6ff } +/* LiteralNumberBin */ +.dark .highlight .chroma .mb { color: #a5d6ff } +/* LiteralNumberFloat */ +.dark .highlight .chroma .mf { color: #a5d6ff } +/* LiteralNumberHex */ +.dark .highlight .chroma .mh { color: #a5d6ff } +/* LiteralNumberInteger */ +.dark .highlight .chroma .mi { color: #a5d6ff } +/* LiteralNumberIntegerLong */ +.dark .highlight .chroma .il { color: #a5d6ff } +/* LiteralNumberOct */ +.dark .highlight .chroma .mo { color: #a5d6ff } +/* Operator */ +.dark .highlight .chroma .o { color: #ff7b72; font-weight: bold } +/* OperatorWord */ +.dark .highlight .chroma .ow { color: #ff7b72; font-weight: bold } +/* Punctuation */ +.dark .highlight .chroma .p { } +/* Comment */ +.dark .highlight .chroma .c { color: #8b949e; font-style: italic } +/* CommentHashbang */ +.dark .highlight .chroma .ch { color: #8b949e; font-style: italic } +/* CommentMultiline */ +.dark .highlight .chroma .cm { color: #8b949e; font-style: italic } +/* CommentSingle */ +.dark .highlight .chroma .c1 { color: #8b949e; font-style: italic } +/* CommentSpecial */ +.dark .highlight .chroma .cs { color: #8b949e; font-weight: bold; font-style: italic } +/* CommentPreproc */ +.dark .highlight .chroma .cp { color: #8b949e; font-weight: bold; font-style: italic } +/* CommentPreprocFile */ +.dark .highlight .chroma .cpf { color: #8b949e; font-weight: bold; font-style: italic } +/* Generic */ +.dark .highlight .chroma .g { } +/* GenericDeleted */ +.dark .highlight .chroma .gd { color: #ffa198; background-color: #490202 } +/* GenericEmph */ +.dark .highlight .chroma .ge { color: inherit; font-style: italic } +/* GenericError */ +.dark .highlight .chroma .gr { color: #ffa198 } +/* GenericHeading */ +.dark .highlight .chroma .gh { color: #79c0ff; font-weight: bold } +/* GenericInserted */ +.dark .highlight .chroma .gi { color: #56d364; background-color: #0f5323 } +/* GenericOutput */ +.dark .highlight .chroma .go { color: #8b949e } +/* GenericPrompt */ +.dark .highlight .chroma .gp { color: #8b949e } +/* GenericStrong */ +.dark .highlight .chroma .gs { font-weight: bold } +/* GenericSubheading */ +.dark .highlight .chroma .gu { color: #79c0ff } +/* GenericTraceback */ +.dark .highlight .chroma .gt { color: #ff7b72 } +/* GenericUnderline */ +.dark .highlight .chroma .gl { text-decoration: underline } +/* TextWhitespace */ +.dark .highlight .chroma .w { color: #6e7681 } +.hextra-code-block { + font-size: .9em; + line-height: 1.25rem; +} +.hextra-code-block pre { + overflow-x: auto; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 39) / 0.05); + font-size: .9em; + font-weight: 500; + -webkit-font-smoothing: auto; + -moz-osx-font-smoothing: auto; +} +@media (prefers-contrast: more) { + + .hextra-code-block pre { + border-width: 1px; + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 24) / 0.2); + --tw-contrast: contrast(1.5); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); + } +} +.hextra-code-block pre:is(html[class~="dark"] *) { + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 27) / 0.1); +} +@media (prefers-contrast: more) { + + .hextra-code-block pre:is(html[class~="dark"] *) { + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 44) / 0.4); + } +} +.hextra-code-block .filename { + position: absolute; + top: 0px; + z-index: 1; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-top-left-radius: 0.75rem; + border-top-right-radius: 0.75rem; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 39) / 0.05); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: .75rem; + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} +.hextra-code-block .filename:is(html[class~="dark"] *) { + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 27) / 0.1); + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} +.hextra-code-block .filename + pre:not(.lntable pre) { + /* Override padding for code blocks with filename but no highlight */ + padding-top: 3rem; + } +.hextra-code-block pre:not(.lntable pre) { + margin-bottom: 1rem; + border-radius: 0.75rem; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 1rem; + padding-bottom: 1rem; +} +.hextra-code-block div:nth-of-type(2) pre { + padding-top: 3rem; + padding-bottom: 1rem; +} +.chroma .lntable { + margin: 0px; + display: block; + width: auto; + overflow: auto; + border-radius: 0.75rem; +} +.chroma .lntable pre { + padding-top: 1rem; + padding-bottom: 1rem; +} +.chroma .ln, + .chroma .lnt:not(.hl > .lnt), + .chroma .hl:not(.line) { + min-width: 2.6rem; + padding-left: 1rem; + padding-right: 1rem; + --tw-text-opacity: 1; + color: rgb(82 82 82 / var(--tw-text-opacity)); +} +.chroma .ln:is(html[class~="dark"] *), + .chroma .lnt:not(.hl > .lnt):is(html[class~="dark"] *), + .chroma .hl:not(.line):is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(212 212 212 / var(--tw-text-opacity)); +} +.chroma .lntd { + padding: 0px; + vertical-align: top; +} +.chroma .lntd:last-of-type { + width: 100%; +} +/* LineHighlight */ +.chroma .hl { + display: block; + width: 100%; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 32) / 0.1); +} +.hextra-cards { + grid-template-columns: repeat(auto-fill, minmax(max(250px, calc((100% - 1rem * 2) / var(--hextra-cards-grid-cols))), 1fr)); +} +.hextra-card { + position: relative; +} +.hextra-card img { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} +.hextra-card:hover svg { + color: currentColor; +} +.hextra-card svg { + width: 1.5rem; + color: #00000033; + transition: color 0.3s ease; +} +.hextra-card p { + margin-top: 0.5rem; + position: relative; +} +.dark .hextra-card svg { + color: #ffffff66; +} +.dark .hextra-card:hover svg { + color: currentColor; +} +.hextra-card-tag { + position: absolute; + top: 5px; + right: 5px; + z-index: 10; +} +.steps h3 { + counter-increment: step; +} +.steps h3:before { + position: absolute; + height: 33px; + width: 33px; + border-width: 4px; + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity)); + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} +.steps h3:is(html[class~="dark"] *):before { + --tw-border-opacity: 1; + border-color: rgb(17 17 17 / var(--tw-border-opacity)); + --tw-bg-opacity: 1; + background-color: rgb(38 38 38 / var(--tw-bg-opacity)); +} +.steps h3:before { + border-radius: 9999px; + text-align: center; + text-indent: -1px; + font-size: 1rem; + font-weight: 400; + --tw-text-opacity: 1; + color: rgb(163 163 163 / var(--tw-text-opacity)); + margin-top: 3px; +} +.steps h3:where([dir="ltr"], [dir="ltr"] *):before { + margin-left: -41px; +} +.steps h3:where([dir="rtl"], [dir="rtl"] *):before { + margin-right: -44px; +} +.steps h3:before { + content: counter(step); + } +:lang(fa) .steps h3:before { + content: counter(step, persian); + } +.search-wrapper li { + margin-left: 0.625rem; + margin-right: 0.625rem; + overflow-wrap: break-word; + border-radius: 0.375rem; + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} +@media (prefers-contrast: more) { + + .search-wrapper li { + border-width: 1px; + border-color: transparent; + } +} +.search-wrapper li:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} +.search-wrapper li a { + display: block; + scroll-margin: 3rem; + padding-left: 0.625rem; + padding-right: 0.625rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.search-wrapper li .title { + font-size: 1rem; + font-weight: 600; + line-height: 1.25rem; +} +.search-wrapper li .active { + border-radius: 0.375rem; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 50) / 0.1); +} +@media (prefers-contrast: more) { + + .search-wrapper li .active { + --tw-border-opacity: 1; + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 50) / var(--tw-border-opacity)); + } +} +.search-wrapper .no-result { + display: block; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + padding: 2rem; + text-align: center; + font-size: .875rem; + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +.search-wrapper .prefix { + margin-left: 0.625rem; + margin-right: 0.625rem; + margin-bottom: 0.5rem; + margin-top: 1.5rem; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + border-bottom-width: 1px; + border-color: rgb(0 0 0 / 0.1); + padding-left: 0.625rem; + padding-right: 0.625rem; + padding-bottom: 0.375rem; + font-size: .75rem; + font-weight: 600; + text-transform: uppercase; + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} +.search-wrapper .prefix:first-child { + margin-top: 0px; +} +@media (prefers-contrast: more) { + + .search-wrapper .prefix { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); + } +} +.search-wrapper .prefix:is(html[class~="dark"] *) { + border-color: rgb(255 255 255 / 0.2); + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} +@media (prefers-contrast: more) { + + .search-wrapper .prefix:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(249 250 251 / var(--tw-border-opacity)); + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); + } +} +.search-wrapper .excerpt { + margin-top: 0.25rem; + overflow: hidden; + text-overflow: ellipsis; + font-size: .875rem; + line-height: 1.35rem; + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} +.search-wrapper .excerpt:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +@media (prefers-contrast: more) { + + .search-wrapper .excerpt:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); + } +} +.search-wrapper .excerpt { + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + } +.search-wrapper .match { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-text-opacity)); +} +@media (max-width: 767px) { + .sidebar-container { + position: fixed; + top: 0px; + bottom: 0px; + z-index: 15; + width: 100%; + overscroll-behavior: contain; + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + padding-top: calc(var(--navbar-height)); + } + .sidebar-container:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(17 17 17 / var(--tw-bg-opacity)); + } + .sidebar-container { + transition: transform 0.8s cubic-bezier(0.52, 0.16, 0.04, 1); + will-change: transform, opacity; + contain: layout style; + backface-visibility: hidden; + } +} +.sidebar-container li > div { + height: 0px; +} +.sidebar-container li.open > div { + height: auto; + padding-top: 0.25rem; +} +.sidebar-container li.open > a > span > svg > path { + --tw-rotate: 90deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} +nav .search-wrapper { + display: none; +} +@media (min-width: 768px) { + + nav .search-wrapper { + display: inline-block; + } +} +@supports ( + ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) +) { + .nav-container-blur { + background-color: rgb(255 255 255 / .85); + --tw-backdrop-blur: blur(12px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + } + .nav-container-blur:is(html[class~="dark"] *) { + background-color: rgb(17 17 17 / 0.8) !important; + } +} +.hamburger-menu svg g { + transform-origin: center; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1); +} +.hamburger-menu svg path { + opacity: 1; + transition: + transform 0.2s cubic-bezier(0.25, 1, 0.5, 1) 0.2s, + opacity 0.2s ease 0.2s; + } +.hamburger-menu svg.open path { + transition: + transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), + opacity 0s ease 0.2s; + } +.hamburger-menu svg.open g { + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1) 0.2s; + } +.hamburger-menu svg.open > path { + opacity: 0; +} +.hamburger-menu svg.open > g:nth-of-type(1) { + --tw-rotate: 45deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} +.hamburger-menu svg.open > g:nth-of-type(1) path { + transform: translate3d(0, 4px, 0); + } +.hamburger-menu svg.open > g:nth-of-type(2) { + --tw-rotate: -45deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} +.hamburger-menu svg.open > g:nth-of-type(2) path { + transform: translate3d(0, -4px, 0); + } +.hextra-scrollbar, .hextra-scrollbar * { + scrollbar-width: thin; /* Firefox */ + scrollbar-color: oklch(55.55% 0 0 / 40%) transparent; /* Firefox */ + + scrollbar-gutter: stable; +} +.hextra-scrollbar::-webkit-scrollbar, .hextra-scrollbar *::-webkit-scrollbar { + height: 0.75rem; + width: 0.75rem; +} +.hextra-scrollbar::-webkit-scrollbar-track, .hextra-scrollbar *::-webkit-scrollbar-track { + background-color: transparent; +} +.hextra-scrollbar::-webkit-scrollbar-thumb, .hextra-scrollbar *::-webkit-scrollbar-thumb { + border-radius: 10px; +} +.hextra-scrollbar:hover::-webkit-scrollbar-thumb, .hextra-scrollbar *:hover::-webkit-scrollbar-thumb { + border: 3px solid transparent; + background-color: var(--tw-shadow-color); + background-clip: content-box; + --tw-shadow-color: rgb(115 115 115 / 0.2); + --tw-shadow: var(--tw-shadow-colored); + } +.hextra-scrollbar:hover::-webkit-scrollbar-thumb:hover, .hextra-scrollbar *:hover::-webkit-scrollbar-thumb:hover { + --tw-shadow-color: rgb(115 115 115 / 0.4); + --tw-shadow: var(--tw-shadow-colored); +} +@supports ( + ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) +) { + .hextra-code-copy-btn { + --tw-bg-opacity: .85; + --tw-backdrop-blur: blur(12px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + } + .hextra-code-copy-btn:is(html[class~="dark"] *) { + --tw-bg-opacity: 0.8; + } +} +@media (min-width: 1024px) { +.hextra-feature-grid { + grid-template-columns: repeat(var(--hextra-feature-grid-cols), minmax(0, 1fr)) +} + } +.hextra-jupyter-code-cell { + scrollbar-gutter: auto; + margin-top: 1.5rem; +} +.hextra-jupyter-code-cell .hextra-jupyter-code-cell-outputs-container { + overflow: hidden; + font-size: .75rem; +} +.hextra-jupyter-code-cell .hextra-jupyter-code-cell-outputs-container .hextra-jupyter-code-cell-outputs { + max-height: 50vh; + overflow: auto; +} +.hextra-jupyter-code-cell .hextra-jupyter-code-cell-outputs-container .hextra-jupyter-code-cell-outputs pre { + max-width: 100%; + overflow: auto; + font-size: .75rem; +} +.hextra-badge { + display: inline-flex; + align-items: center; +} +html { + font-size: 1rem; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: "rlig" 1, "calt" 1, "ss01" 1; + -webkit-tap-highlight-color: transparent; +} +body { + width: 100%; + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} +body:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(17 17 17 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); +} +:root { + --primary-hue: 212deg; + --primary-saturation: 100%; + --primary-lightness: 50%; + --navbar-height: 4rem; + --menu-height: 3.75rem; +} +.dark { + --primary-hue: 204deg; + --primary-saturation: 100%; + --primary-lightness: 50%; +} +.placeholder\:hx-text-gray-500::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} +.placeholder\:hx-text-gray-500::placeholder { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} +.before\:hx-pointer-events-none::before { + content: var(--tw-content); + pointer-events: none; +} +.before\:hx-absolute::before { + content: var(--tw-content); + position: absolute; +} +.before\:hx-inset-0::before { + content: var(--tw-content); + inset: 0px; +} +.before\:hx-inset-y-1::before { + content: var(--tw-content); + top: 0.25rem; + bottom: 0.25rem; +} +.before\:hx-mr-1::before { + content: var(--tw-content); + margin-right: 0.25rem; +} +.before\:hx-inline-block::before { + content: var(--tw-content); + display: inline-block; +} +.before\:hx-w-px::before { + content: var(--tw-content); + width: 1px; +} +.before\:hx-bg-gray-200::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} +.before\:hx-opacity-25::before { + content: var(--tw-content); + opacity: 0.25; +} +.before\:hx-transition-transform::before { + content: var(--tw-content); + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.before\:hx-content-\[\'\#\'\]::before { + --tw-content: '#'; + content: var(--tw-content); +} +.before\:hx-content-\[\'\'\]::before { + --tw-content: ''; + content: var(--tw-content); +} +.before\:hx-content-\[\\\"\\\"\]::before { + --tw-content: \"\"; + content: var(--tw-content); +} +.first\:hx-mt-0:first-child { + margin-top: 0px; +} +.last-of-type\:hx-mb-0:last-of-type { + margin-bottom: 0px; +} +.hover\:hx-border-gray-200:hover { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); +} +.hover\:hx-border-gray-300:hover { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} +.hover\:hx-border-gray-400:hover { + --tw-border-opacity: 1; + border-color: rgb(156 163 175 / var(--tw-border-opacity)); +} +.hover\:hx-border-gray-900:hover { + --tw-border-opacity: 1; + border-color: rgb(17 24 39 / var(--tw-border-opacity)); +} +.hover\:hx-bg-gray-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} +.hover\:hx-bg-gray-800\/5:hover { + background-color: rgb(31 41 55 / 0.05); +} +.hover\:hx-bg-primary-50:hover { + --tw-bg-opacity: 1; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 47) / var(--tw-bg-opacity)); +} +.hover\:hx-bg-primary-700:hover { + --tw-bg-opacity: 1; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 39) / var(--tw-bg-opacity)); +} +.hover\:hx-bg-slate-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(248 250 252 / var(--tw-bg-opacity)); +} +.hover\:hx-text-black:hover { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} +.hover\:hx-text-gray-800:hover { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} +.hover\:hx-text-gray-900:hover { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} +.hover\:hx-text-primary-600:hover { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-text-opacity)); +} +.hover\:hx-opacity-60:hover { + opacity: 0.6; +} +.hover\:hx-opacity-75:hover { + opacity: 0.75; +} +.hover\:hx-shadow-lg:hover { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hover\:hx-shadow-md:hover { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.hover\:hx-shadow-gray-100:hover { + --tw-shadow-color: #f3f4f6; + --tw-shadow: var(--tw-shadow-colored); +} +.focus\:hx-bg-white:focus { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} +.focus\:hx-outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} +.focus\:hx-ring-4:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} +.focus\:hx-ring-primary-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 27) / var(--tw-ring-opacity)); +} +.active\:hx-bg-gray-400\/20:active { + background-color: rgb(156 163 175 / 0.2); +} +.active\:hx-opacity-50:active { + opacity: 0.5; +} +.active\:hx-shadow-sm:active { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.active\:hx-shadow-gray-200:active { + --tw-shadow-color: #e5e7eb; + --tw-shadow: var(--tw-shadow-colored); +} +.hx-group[open] .group-open\:before\:hx-rotate-90::before { + content: var(--tw-content); + --tw-rotate: 90deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} +.hx-group:hover .group-hover\:hx-underline { + text-decoration-line: underline; +} +.hx-group\/code:hover .group-hover\/code\:hx-opacity-100 { + opacity: 1; +} +.hx-group\/copybtn.copied .group-\[\.copied\]\/copybtn\:hx-block { + display: block; +} +.hx-group\/copybtn.copied .group-\[\.copied\]\/copybtn\:hx-hidden { + display: none; +} +.data-\[state\=selected\]\:hx-block[data-state="selected"] { + display: block; +} +.data-\[state\=closed\]\:hx-hidden[data-state="closed"] { + display: none; +} +.data-\[state\=open\]\:hx-hidden[data-state="open"] { + display: none; +} +.data-\[state\=selected\]\:hx-border-primary-500[data-state="selected"] { + --tw-border-opacity: 1; + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 50) / var(--tw-border-opacity)); +} +.data-\[state\=selected\]\:hx-text-primary-600[data-state="selected"] { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-text-opacity)); +} +.hx-group[data-theme="dark"] .group-data-\[theme\=dark\]\:hx-hidden { + display: none; +} +.hx-group[data-theme="light"] .group-data-\[theme\=light\]\:hx-hidden { + display: none; +} +@media (prefers-contrast: more) { + + .contrast-more\:hx-border { + border-width: 1px; + } + + .contrast-more\:hx-border-t { + border-top-width: 1px; + } + + .contrast-more\:hx-border-current { + border-color: currentColor; + } + + .contrast-more\:hx-border-gray-800 { + --tw-border-opacity: 1; + border-color: rgb(31 41 55 / var(--tw-border-opacity)); + } + + .contrast-more\:hx-border-gray-900 { + --tw-border-opacity: 1; + border-color: rgb(17 24 39 / var(--tw-border-opacity)); + } + + .contrast-more\:hx-border-neutral-400 { + --tw-border-opacity: 1; + border-color: rgb(163 163 163 / var(--tw-border-opacity)); + } + + .contrast-more\:hx-border-primary-500 { + --tw-border-opacity: 1; + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 50) / var(--tw-border-opacity)); + } + + .contrast-more\:hx-border-transparent { + border-color: transparent; + } + + .contrast-more\:hx-font-bold { + font-weight: 700; + } + + .contrast-more\:hx-text-current { + color: currentColor; + } + + .contrast-more\:hx-text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); + } + + .contrast-more\:hx-text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); + } + + .contrast-more\:hx-text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); + } + + .contrast-more\:hx-underline { + text-decoration-line: underline; + } + + .contrast-more\:hx-shadow-\[0_0_0_1px_\#000\] { + --tw-shadow: 0 0 0 1px #000; + --tw-shadow-colored: 0 0 0 1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + .contrast-more\:hx-shadow-none { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + .contrast-more\:hover\:hx-border-gray-900:hover { + --tw-border-opacity: 1; + border-color: rgb(17 24 39 / var(--tw-border-opacity)); + } +} +.dark\:hx-block:is(html[class~="dark"] *) { + display: block; +} +.dark\:hx-hidden:is(html[class~="dark"] *) { + display: none; +} +.dark\:hx-border-amber-200\/30:is(html[class~="dark"] *) { + border-color: rgb(253 230 138 / 0.3); +} +.dark\:hx-border-blue-200\/30:is(html[class~="dark"] *) { + border-color: rgb(191 219 254 / 0.3); +} +.dark\:hx-border-gray-100\/20:is(html[class~="dark"] *) { + border-color: rgb(243 244 246 / 0.2); +} +.dark\:hx-border-gray-400:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(156 163 175 / var(--tw-border-opacity)); +} +.dark\:hx-border-green-200\/30:is(html[class~="dark"] *) { + border-color: rgb(187 247 208 / 0.3); +} +.dark\:hx-border-indigo-200\/30:is(html[class~="dark"] *) { + border-color: rgb(199 210 254 / 0.3); +} +.dark\:hx-border-neutral-700:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(64 64 64 / var(--tw-border-opacity)); +} +.dark\:hx-border-neutral-800:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(38 38 38 / var(--tw-border-opacity)); +} +.dark\:hx-border-orange-400\/30:is(html[class~="dark"] *) { + border-color: rgb(251 146 60 / 0.3); +} +.dark\:hx-border-red-200\/30:is(html[class~="dark"] *) { + border-color: rgb(254 202 202 / 0.3); +} +.dark\:hx-border-white\/10:is(html[class~="dark"] *) { + border-color: rgb(255 255 255 / 0.1); +} +.dark\:hx-border-yellow-200\/30:is(html[class~="dark"] *) { + border-color: rgb(254 240 138 / 0.3); +} +.dark\:hx-bg-amber-900\/30:is(html[class~="dark"] *) { + background-color: rgb(120 53 15 / 0.3); +} +.dark\:hx-bg-black\/60:is(html[class~="dark"] *) { + background-color: rgb(0 0 0 / 0.6); +} +.dark\:hx-bg-blue-900\/30:is(html[class~="dark"] *) { + background-color: rgb(30 58 138 / 0.3); +} +.dark\:hx-bg-dark:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(17 17 17 / var(--tw-bg-opacity)); +} +.dark\:hx-bg-dark\/50:is(html[class~="dark"] *) { + background-color: rgb(17 17 17 / 0.5); +} +.dark\:hx-bg-gray-50\/10:is(html[class~="dark"] *) { + background-color: rgb(249 250 251 / 0.1); +} +.dark\:hx-bg-green-900\/30:is(html[class~="dark"] *) { + background-color: rgb(20 83 45 / 0.3); +} +.dark\:hx-bg-indigo-900\/30:is(html[class~="dark"] *) { + background-color: rgb(49 46 129 / 0.3); +} +.dark\:hx-bg-neutral-800:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(38 38 38 / var(--tw-bg-opacity)); +} +.dark\:hx-bg-neutral-900:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(23 23 23 / var(--tw-bg-opacity)); +} +.dark\:hx-bg-orange-400\/20:is(html[class~="dark"] *) { + background-color: rgb(251 146 60 / 0.2); +} +.dark\:hx-bg-primary-300\/10:is(html[class~="dark"] *) { + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 27) / 0.1); +} +.dark\:hx-bg-primary-400\/10:is(html[class~="dark"] *) { + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 16) / 0.1); +} +.dark\:hx-bg-primary-600:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-bg-opacity)); +} +.dark\:hx-bg-red-900\/30:is(html[class~="dark"] *) { + background-color: rgb(127 29 29 / 0.3); +} +.dark\:hx-bg-yellow-700\/30:is(html[class~="dark"] *) { + background-color: rgb(161 98 7 / 0.3); +} +.dark\:hx-from-gray-100:is(html[class~="dark"] *) { + --tw-gradient-from: #f3f4f6 var(--tw-gradient-from-position); + --tw-gradient-to: rgb(243 244 246 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} +.dark\:hx-to-gray-400:is(html[class~="dark"] *) { + --tw-gradient-to: #9ca3af var(--tw-gradient-to-position); +} +.dark\:hx-text-amber-200:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(253 230 138 / var(--tw-text-opacity)); +} +.dark\:hx-text-blue-200:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(191 219 254 / var(--tw-text-opacity)); +} +.dark\:hx-text-gray-100:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); +} +.dark\:hx-text-gray-200:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} +.dark\:hx-text-gray-300:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} +.dark\:hx-text-gray-400:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +.dark\:hx-text-gray-50:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); +} +.dark\:hx-text-green-200:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(187 247 208 / var(--tw-text-opacity)); +} +.dark\:hx-text-indigo-200:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(199 210 254 / var(--tw-text-opacity)); +} +.dark\:hx-text-neutral-200:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(229 229 229 / var(--tw-text-opacity)); +} +.dark\:hx-text-neutral-400:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(163 163 163 / var(--tw-text-opacity)); +} +.dark\:hx-text-orange-300:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(253 186 116 / var(--tw-text-opacity)); +} +.dark\:hx-text-primary-600:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-text-opacity)); +} +.dark\:hx-text-red-200:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(254 202 202 / var(--tw-text-opacity)); +} +.dark\:hx-text-slate-100:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(241 245 249 / var(--tw-text-opacity)); +} +.dark\:hx-text-yellow-200:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(254 240 138 / var(--tw-text-opacity)); +} +.dark\:hx-opacity-80:is(html[class~="dark"] *) { + opacity: 0.8; +} +.dark\:hx-shadow-\[0_-12px_16px_\#111\]:is(html[class~="dark"] *) { + --tw-shadow: 0 -12px 16px #111; + --tw-shadow-colored: 0 -12px 16px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.dark\:hx-shadow-\[0_-1px_0_rgba\(255\2c 255\2c 255\2c \.1\)_inset\]:is(html[class~="dark"] *) { + --tw-shadow: 0 -1px 0 rgba(255,255,255,.1) inset; + --tw-shadow-colored: inset 0 -1px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.dark\:hx-shadow-none:is(html[class~="dark"] *) { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.dark\:hx-ring-white\/20:is(html[class~="dark"] *) { + --tw-ring-color: rgb(255 255 255 / 0.2); +} +.dark\:placeholder\:hx-text-gray-400:is(html[class~="dark"] *)::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +.dark\:placeholder\:hx-text-gray-400:is(html[class~="dark"] *)::placeholder { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +.dark\:before\:hx-bg-neutral-800:is(html[class~="dark"] *)::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(38 38 38 / var(--tw-bg-opacity)); +} +.dark\:before\:hx-invert:is(html[class~="dark"] *)::before { + content: var(--tw-content); + --tw-invert: invert(100%); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} +.dark\:hover\:hx-border-gray-100:hover:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity)); +} +.dark\:hover\:hx-border-gray-600:hover:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); +} +.dark\:hover\:hx-border-neutral-500:hover:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(115 115 115 / var(--tw-border-opacity)); +} +.dark\:hover\:hx-border-neutral-700:hover:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(64 64 64 / var(--tw-border-opacity)); +} +.dark\:hover\:hx-border-neutral-800:hover:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(38 38 38 / var(--tw-border-opacity)); +} +.dark\:hover\:hx-bg-gray-100\/5:hover:is(html[class~="dark"] *) { + background-color: rgb(243 244 246 / 0.05); +} +.dark\:hover\:hx-bg-neutral-700:hover:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(64 64 64 / var(--tw-bg-opacity)); +} +.dark\:hover\:hx-bg-neutral-800:hover:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(38 38 38 / var(--tw-bg-opacity)); +} +.dark\:hover\:hx-bg-neutral-900:hover:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(23 23 23 / var(--tw-bg-opacity)); +} +.dark\:hover\:hx-bg-primary-100\/5:hover:is(html[class~="dark"] *) { + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 44) / 0.05); +} +.dark\:hover\:hx-bg-primary-700:hover:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 39) / var(--tw-bg-opacity)); +} +.hover\:dark\:hx-bg-primary-500\/10:is(html[class~="dark"] *):hover { + background-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 50) / 0.1); +} +.dark\:hover\:hx-text-gray-100:hover:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); +} +.dark\:hover\:hx-text-gray-200:hover:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} +.dark\:hover\:hx-text-gray-300:hover:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} +.dark\:hover\:hx-text-gray-50:hover:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); +} +.dark\:hover\:hx-text-neutral-50:hover:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(250 250 250 / var(--tw-text-opacity)); +} +.dark\:hover\:hx-text-white:hover:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} +.hover\:dark\:hx-text-primary-600:is(html[class~="dark"] *):hover { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-text-opacity)); +} +.dark\:hover\:hx-shadow-none:hover:is(html[class~="dark"] *) { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +.dark\:focus\:hx-bg-dark:focus:is(html[class~="dark"] *) { + --tw-bg-opacity: 1; + background-color: rgb(17 17 17 / var(--tw-bg-opacity)); +} +.dark\:focus\:hx-ring-primary-800:focus:is(html[class~="dark"] *) { + --tw-ring-opacity: 1; + --tw-ring-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 32) / var(--tw-ring-opacity)); +} +.data-\[state\=selected\]\:dark\:hx-border-primary-500:is(html[class~="dark"] *)[data-state="selected"] { + --tw-border-opacity: 1; + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 50) / var(--tw-border-opacity)); +} +.data-\[state\=selected\]\:dark\:hx-text-primary-600:is(html[class~="dark"] *)[data-state="selected"] { + --tw-text-opacity: 1; + color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45) / var(--tw-text-opacity)); +} +@media (prefers-contrast: more) { + + .contrast-more\:dark\:hx-border-current:is(html[class~="dark"] *) { + border-color: currentColor; + } + + .contrast-more\:dark\:hx-border-gray-50:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(249 250 251 / var(--tw-border-opacity)); + } + + .contrast-more\:dark\:hx-border-neutral-400:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(163 163 163 / var(--tw-border-opacity)); + } + + .contrast-more\:dark\:hx-border-primary-500:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 50) / var(--tw-border-opacity)); + } + + .dark\:contrast-more\:hx-border-neutral-400:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(163 163 163 / var(--tw-border-opacity)); + } + + .contrast-more\:dark\:hx-text-current:is(html[class~="dark"] *) { + color: currentColor; + } + + .contrast-more\:dark\:hx-text-gray-100:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); + } + + .contrast-more\:dark\:hx-text-gray-300:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); + } + + .contrast-more\:dark\:hx-text-gray-50:is(html[class~="dark"] *) { + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); + } + + .contrast-more\:dark\:hx-shadow-\[0_0_0_1px_\#fff\]:is(html[class~="dark"] *) { + --tw-shadow: 0 0 0 1px #fff; + --tw-shadow-colored: 0 0 0 1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + .contrast-more\:dark\:hx-shadow-none:is(html[class~="dark"] *) { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + .contrast-more\:dark\:hover\:hx-border-gray-50:hover:is(html[class~="dark"] *) { + --tw-border-opacity: 1; + border-color: rgb(249 250 251 / var(--tw-border-opacity)); + } +} +@media not all and (min-width: 1280px) { + + .max-xl\:hx-hidden { + display: none; + } +} +@media not all and (min-width: 1024px) { + + .max-lg\:hx-min-h-\[340px\] { + min-height: 340px; + } +} +@media not all and (min-width: 768px) { + + .max-md\:hx-hidden { + display: none; + } + + .max-md\:hx-min-h-\[340px\] { + min-height: 340px; + } + + .max-md\:\[transform\:translate3d\(0\2c -100\%\2c 0\)\] { + transform: translate3d(0,-100%,0); + } +} +@media not all and (min-width: 640px) { + + .max-sm\:hx-grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } +} +@media (min-width: 640px) { + + .sm\:hx-block { + display: block; + } + + .sm\:hx-flex { + display: flex; + } + + .sm\:hx-w-\[110\%\] { + width: 110%; + } + + .sm\:hx-items-start { + align-items: flex-start; + } + + .sm\:hx-text-xl { + font-size: 1.25rem; + } + + @media not all and (min-width: 1024px) { + + .sm\:max-lg\:hx-grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } +} +@media (min-width: 768px) { + + .md\:hx-sticky { + position: sticky; + } + + .md\:hx-top-16 { + top: 4rem; + } + + .md\:hx-inline-block { + display: inline-block; + } + + .md\:hx-hidden { + display: none; + } + + .md\:hx-aspect-\[1\.1\/1\] { + aspect-ratio: 1.1/1; + } + + .md\:hx-h-\[calc\(100vh-var\(--navbar-height\)-var\(--menu-height\)\)\] { + height: calc(100vh - var(--navbar-height) - var(--menu-height)); + } + + .md\:hx-max-h-\[min\(calc\(100vh-5rem-env\(safe-area-inset-bottom\)\)\2c 400px\)\] { + max-height: min(calc(100vh - 5rem - env(safe-area-inset-bottom)),400px); + } + + .md\:hx-w-64 { + width: 16rem; + } + + .md\:hx-shrink-0 { + flex-shrink: 0; + } + + .md\:hx-grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .md\:hx-justify-start { + justify-content: flex-start; + } + + .md\:hx-self-start { + align-self: flex-start; + } + + .md\:hx-px-12 { + padding-left: 3rem; + padding-right: 3rem; + } + + .md\:hx-pt-12 { + padding-top: 3rem; + } + + .md\:hx-text-5xl { + font-size: 3rem; + } + + .md\:hx-text-lg { + font-size: 1.125rem; + } + + .md\:hx-text-sm { + font-size: .875rem; + } +} +@media (min-width: 1024px) { + + .lg\:hx-grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} +@media (min-width: 1280px) { + + .xl\:hx-block { + display: block; + } + + .xl\:hx-grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} +.ltr\:hx-right-1\.5:where([dir="ltr"], [dir="ltr"] *) { + right: 0.375rem; +} +.ltr\:hx-right-3:where([dir="ltr"], [dir="ltr"] *) { + right: 0.75rem; +} +.ltr\:hx--mr-4:where([dir="ltr"], [dir="ltr"] *) { + margin-right: -1rem; +} +.ltr\:hx-ml-1:where([dir="ltr"], [dir="ltr"] *) { + margin-left: 0.25rem; +} +.ltr\:hx-ml-3:where([dir="ltr"], [dir="ltr"] *) { + margin-left: 0.75rem; +} +.ltr\:hx-ml-auto:where([dir="ltr"], [dir="ltr"] *) { + margin-left: auto; +} +.ltr\:hx-mr-auto:where([dir="ltr"], [dir="ltr"] *) { + margin-right: auto; +} +.ltr\:hx-rotate-180:where([dir="ltr"], [dir="ltr"] *) { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} +.ltr\:hx-border-l:where([dir="ltr"], [dir="ltr"] *) { + border-left-width: 1px; +} +.ltr\:hx-pl-12:where([dir="ltr"], [dir="ltr"] *) { + padding-left: 3rem; +} +.ltr\:hx-pl-16:where([dir="ltr"], [dir="ltr"] *) { + padding-left: 4rem; +} +.ltr\:hx-pl-3:where([dir="ltr"], [dir="ltr"] *) { + padding-left: 0.75rem; +} +.ltr\:hx-pl-4:where([dir="ltr"], [dir="ltr"] *) { + padding-left: 1rem; +} +.ltr\:hx-pl-5:where([dir="ltr"], [dir="ltr"] *) { + padding-left: 1.25rem; +} +.ltr\:hx-pl-6:where([dir="ltr"], [dir="ltr"] *) { + padding-left: 1.5rem; +} +.ltr\:hx-pl-8:where([dir="ltr"], [dir="ltr"] *) { + padding-left: 2rem; +} +.ltr\:hx-pr-0:where([dir="ltr"], [dir="ltr"] *) { + padding-right: 0px; +} +.ltr\:hx-pr-2:where([dir="ltr"], [dir="ltr"] *) { + padding-right: 0.5rem; +} +.ltr\:hx-pr-4:where([dir="ltr"], [dir="ltr"] *) { + padding-right: 1rem; +} +.ltr\:hx-pr-9:where([dir="ltr"], [dir="ltr"] *) { + padding-right: 2.25rem; +} +.ltr\:hx-text-right:where([dir="ltr"], [dir="ltr"] *) { + text-align: right; +} +.ltr\:before\:hx-left-0:where([dir="ltr"], [dir="ltr"] *)::before { + content: var(--tw-content); + left: 0px; +} +@media (min-width: 768px) { + + .ltr\:md\:hx-left-auto:where([dir="ltr"], [dir="ltr"] *) { + left: auto; + } +} +.rtl\:hx-left-1\.5:where([dir="rtl"], [dir="rtl"] *) { + left: 0.375rem; +} +.rtl\:hx-left-3:where([dir="rtl"], [dir="rtl"] *) { + left: 0.75rem; +} +.rtl\:hx--ml-4:where([dir="rtl"], [dir="rtl"] *) { + margin-left: -1rem; +} +.rtl\:hx-ml-auto:where([dir="rtl"], [dir="rtl"] *) { + margin-left: auto; +} +.rtl\:hx-mr-1:where([dir="rtl"], [dir="rtl"] *) { + margin-right: 0.25rem; +} +.rtl\:hx-mr-3:where([dir="rtl"], [dir="rtl"] *) { + margin-right: 0.75rem; +} +.rtl\:hx-mr-auto:where([dir="rtl"], [dir="rtl"] *) { + margin-right: auto; +} +.rtl\:-hx-rotate-180:where([dir="rtl"], [dir="rtl"] *) { + --tw-rotate: -180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} +.rtl\:hx-border-r:where([dir="rtl"], [dir="rtl"] *) { + border-right-width: 1px; +} +.rtl\:hx-pl-2:where([dir="rtl"], [dir="rtl"] *) { + padding-left: 0.5rem; +} +.rtl\:hx-pl-4:where([dir="rtl"], [dir="rtl"] *) { + padding-left: 1rem; +} +.rtl\:hx-pl-9:where([dir="rtl"], [dir="rtl"] *) { + padding-left: 2.25rem; +} +.rtl\:hx-pr-12:where([dir="rtl"], [dir="rtl"] *) { + padding-right: 3rem; +} +.rtl\:hx-pr-16:where([dir="rtl"], [dir="rtl"] *) { + padding-right: 4rem; +} +.rtl\:hx-pr-3:where([dir="rtl"], [dir="rtl"] *) { + padding-right: 0.75rem; +} +.rtl\:hx-pr-4:where([dir="rtl"], [dir="rtl"] *) { + padding-right: 1rem; +} +.rtl\:hx-pr-5:where([dir="rtl"], [dir="rtl"] *) { + padding-right: 1.25rem; +} +.rtl\:hx-pr-6:where([dir="rtl"], [dir="rtl"] *) { + padding-right: 1.5rem; +} +.rtl\:hx-pr-8:where([dir="rtl"], [dir="rtl"] *) { + padding-right: 2rem; +} +.rtl\:hx-text-left:where([dir="rtl"], [dir="rtl"] *) { + text-align: left; +} +.rtl\:before\:hx-right-0:where([dir="rtl"], [dir="rtl"] *)::before { + content: var(--tw-content); + right: 0px; +} +.rtl\:before\:hx-rotate-180:where([dir="rtl"], [dir="rtl"] *)::before { + content: var(--tw-content); + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} +@media (min-width: 768px) { + + .rtl\:md\:hx-right-auto:where([dir="rtl"], [dir="rtl"] *) { + right: auto; + } +} +@media print { + + .print\:hx-hidden { + display: none; + } + + .print\:hx-bg-transparent { + background-color: transparent; + } +} diff --git a/css/compiled/main.min.e0bb0f28123bf084d555fc9253c6d2aa1443b2dfcde6044741a207eabdc3ddb8.css b/css/compiled/main.min.e0bb0f28123bf084d555fc9253c6d2aa1443b2dfcde6044741a207eabdc3ddb8.css new file mode 100644 index 0000000..823130e --- /dev/null +++ b/css/compiled/main.min.e0bb0f28123bf084d555fc9253c6d2aa1443b2dfcde6044741a207eabdc3ddb8.css @@ -0,0 +1 @@ +*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::before,::after{--tw-content:''}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,liberation mono,courier new,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::before,::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.hx-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.hx-pointer-events-none{pointer-events:none}.hx-fixed{position:fixed}.hx-absolute{position:absolute}.hx-relative{position:relative}.hx-sticky{position:sticky}.hx-inset-0{inset:0}.hx-inset-x-0{left:0;right:0}.hx-inset-y-0{top:0;bottom:0}.hx-bottom-0{bottom:0}.hx-left-\[24px\]{left:24px}.hx-left-\[36px\]{left:36px}.hx-right-0{right:0}.hx-top-0{top:0}.hx-top-16{top:4rem}.hx-top-8{top:2rem}.hx-top-\[40\%\]{top:40%}.hx-top-full{top:100%}.hx-z-10{z-index:10}.hx-z-20{z-index:20}.hx-z-\[-1\]{z-index:-1}.hx-order-last{order:9999}.hx-m-\[11px\]{margin:11px}.hx-mx-1{margin-left:.25rem;margin-right:.25rem}.hx-mx-4{margin-left:1rem;margin-right:1rem}.hx-mx-auto{margin-left:auto;margin-right:auto}.hx-my-1\.5{margin-top:.375rem;margin-bottom:.375rem}.hx-my-2{margin-top:.5rem;margin-bottom:.5rem}.-hx-mb-0\.5{margin-bottom:-.125rem}.-hx-ml-2{margin-left:-.5rem}.-hx-mr-2{margin-right:-.5rem}.-hx-mt-20{margin-top:-5rem}.hx-mb-10{margin-bottom:2.5rem}.hx-mb-12{margin-bottom:3rem}.hx-mb-16{margin-bottom:4rem}.hx-mb-2{margin-bottom:.5rem}.hx-mb-4{margin-bottom:1rem}.hx-mb-6{margin-bottom:1.5rem}.hx-mb-8{margin-bottom:2rem}.hx-ml-4{margin-left:1rem}.hx-mr-1{margin-right:.25rem}.hx-mr-2{margin-right:.5rem}.hx-mt-1{margin-top:.25rem}.hx-mt-1\.5{margin-top:.375rem}.hx-mt-12{margin-top:3rem}.hx-mt-16{margin-top:4rem}.hx-mt-2{margin-top:.5rem}.hx-mt-4{margin-top:1rem}.hx-mt-5{margin-top:1.25rem}.hx-mt-6{margin-top:1.5rem}.hx-mt-8{margin-top:2rem}.hx-line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}.hx-block{display:block}.hx-inline-block{display:inline-block}.hx-inline{display:inline}.hx-flex{display:flex}.hx-inline-flex{display:inline-flex}.hx-grid{display:grid}.hx-hidden{display:none}.hx-aspect-auto{aspect-ratio:auto}.hx-h-0{height:0}.hx-h-16{height:4rem}.hx-h-2{height:.5rem}.hx-h-3\.5{height:.875rem}.hx-h-4{height:1rem}.hx-h-5{height:1.25rem}.hx-h-7{height:1.75rem}.hx-h-\[18px\]{height:18px}.hx-h-full{height:100%}.hx-max-h-64{max-height:16rem}.hx-max-h-\[calc\(100vh-var\(--navbar-height\)-env\(safe-area-inset-bottom\)\)\]{max-height:calc(100vh - var(--navbar-height) - env(safe-area-inset-bottom))}.hx-max-h-\[min\(calc\(50vh-11rem-env\(safe-area-inset-bottom\)\)\,400px\)\]{max-height:min(calc(50vh - 11rem - env(safe-area-inset-bottom)),400px)}.hx-min-h-\[100px\]{min-height:100px}.hx-min-h-\[calc\(100vh-var\(--navbar-height\)\)\]{min-height:calc(100vh - var(--navbar-height))}.hx-w-2{width:.5rem}.hx-w-3\.5{width:.875rem}.hx-w-4{width:1rem}.hx-w-64{width:16rem}.hx-w-\[110\%\]{width:110%}.hx-w-\[180\%\]{width:180%}.hx-w-full{width:100%}.hx-w-max{width:-moz-max-content;width:max-content}.hx-w-screen{width:100vw}.hx-min-w-0{min-width:0}.hx-min-w-\[18px\]{min-width:18px}.hx-min-w-\[24px\]{min-width:24px}.hx-min-w-full{min-width:100%}.hx-max-w-6xl{max-width:72rem}.hx-max-w-\[50\%\]{max-width:50%}.hx-max-w-\[90rem\]{max-width:90rem}.hx-max-w-\[min\(calc\(100vw-2rem\)\,calc\(100\%\+20rem\)\)\]{max-width:min(calc(100vw - 2rem),calc(100% + 20rem))}.hx-max-w-none{max-width:none}.hx-max-w-screen-xl{max-width:1280px}.hx-shrink-0{flex-shrink:0}.hx-grow{flex-grow:1}.hx-origin-center{transform-origin:center}.hx-cursor-default{cursor:default}.hx-cursor-pointer{cursor:pointer}.hx-select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.hx-scroll-my-6{scroll-margin-top:1.5rem;scroll-margin-bottom:1.5rem}.hx-scroll-py-6{scroll-padding-top:1.5rem;scroll-padding-bottom:1.5rem}.hx-list-none{list-style-type:none}.hx-appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.hx-grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.hx-flex-col{flex-direction:column}.hx-flex-wrap{flex-wrap:wrap}.hx-items-start{align-items:flex-start}.hx-items-center{align-items:center}.hx-justify-start{justify-content:flex-start}.hx-justify-end{justify-content:flex-end}.hx-justify-center{justify-content:center}.hx-justify-between{justify-content:space-between}.hx-justify-items-start{justify-items:start}.hx-gap-1{gap:.25rem}.hx-gap-2{gap:.5rem}.hx-gap-4{gap:1rem}.hx-gap-x-1\.5{-moz-column-gap:.375rem;column-gap:.375rem}.hx-gap-y-2{row-gap:.5rem}.hx-overflow-auto{overflow:auto}.hx-overflow-hidden{overflow:hidden}.hx-overflow-x-auto{overflow-x:auto}.hx-overflow-y-auto{overflow-y:auto}.hx-overflow-x-hidden{overflow-x:hidden}.hx-overflow-y-hidden{overflow-y:hidden}.hx-overscroll-contain{overscroll-behavior:contain}.hx-overscroll-x-contain{overscroll-behavior-x:contain}.hx-text-ellipsis{text-overflow:ellipsis}.hx-whitespace-nowrap{white-space:nowrap}.hx-break-words{overflow-wrap:break-word}.hx-rounded{border-radius:.25rem}.hx-rounded-3xl{border-radius:1.5rem}.hx-rounded-full{border-radius:9999px}.hx-rounded-lg{border-radius:.5rem}.hx-rounded-md{border-radius:.375rem}.hx-rounded-sm{border-radius:.125rem}.hx-rounded-xl{border-radius:.75rem}.hx-rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.hx-border{border-width:1px}.hx-border-b{border-bottom-width:1px}.hx-border-b-2{border-bottom-width:2px}.hx-border-t{border-top-width:1px}.hx-border-amber-200{--tw-border-opacity:1;border-color:rgb(253 230 138/var(--tw-border-opacity))}.hx-border-black\/5{border-color:rgb(0 0 0/5%)}.hx-border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity))}.hx-border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.hx-border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.hx-border-green-200{--tw-border-opacity:1;border-color:rgb(187 247 208/var(--tw-border-opacity))}.hx-border-indigo-200{--tw-border-opacity:1;border-color:rgb(199 210 254/var(--tw-border-opacity))}.hx-border-orange-100{--tw-border-opacity:1;border-color:rgb(255 237 213/var(--tw-border-opacity))}.hx-border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity))}.hx-border-transparent{border-color:transparent}.hx-border-yellow-100{--tw-border-opacity:1;border-color:rgb(254 249 195/var(--tw-border-opacity))}.hx-bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity))}.hx-bg-black\/80{background-color:rgb(0 0 0/.8)}.hx-bg-black\/\[\.05\]{background-color:rgb(0 0 0/5%)}.hx-bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.hx-bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hx-bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity))}.hx-bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity))}.hx-bg-neutral-50{--tw-bg-opacity:1;background-color:rgb(250 250 250/var(--tw-bg-opacity))}.hx-bg-orange-50{--tw-bg-opacity:1;background-color:rgb(255 247 237/var(--tw-bg-opacity))}.hx-bg-primary-100{--tw-bg-opacity:1;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 44)/var(--tw-bg-opacity))}.hx-bg-primary-400{--tw-bg-opacity:1;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 16)/var(--tw-bg-opacity))}.hx-bg-primary-600{--tw-bg-opacity:1;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-bg-opacity))}.hx-bg-primary-700\/5{background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 39)/.05)}.hx-bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.hx-bg-transparent{background-color:transparent}.hx-bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hx-bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.hx-bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.hx-from-gray-900{--tw-gradient-from:#111827 var(--tw-gradient-from-position);--tw-gradient-to:rgb(17 24 39 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.hx-to-gray-600{--tw-gradient-to:#4b5563 var(--tw-gradient-to-position)}.hx-bg-clip-text{-webkit-background-clip:text;background-clip:text}.hx-p-0\.5{padding:.125rem}.hx-p-1{padding:.25rem}.hx-p-1\.5{padding:.375rem}.hx-p-2{padding:.5rem}.hx-p-4{padding:1rem}.hx-p-6{padding:1.5rem}.hx-px-1\.5{padding-left:.375rem;padding-right:.375rem}.hx-px-2{padding-left:.5rem;padding-right:.5rem}.hx-px-2\.5{padding-left:.625rem;padding-right:.625rem}.hx-px-3{padding-left:.75rem;padding-right:.75rem}.hx-px-4{padding-left:1rem;padding-right:1rem}.hx-px-6{padding-left:1.5rem;padding-right:1.5rem}.hx-py-1{padding-top:.25rem;padding-bottom:.25rem}.hx-py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.hx-py-12{padding-top:3rem;padding-bottom:3rem}.hx-py-2{padding-top:.5rem;padding-bottom:.5rem}.hx-py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.hx-py-3{padding-top:.75rem;padding-bottom:.75rem}.hx-py-4{padding-top:1rem;padding-bottom:1rem}.hx-pb-8{padding-bottom:2rem}.hx-pb-\[env\(safe-area-inset-bottom\)\]{padding-bottom:env(safe-area-inset-bottom)}.hx-pb-px{padding-bottom:1px}.hx-pl-\[max\(env\(safe-area-inset-left\)\,1\.5rem\)\]{padding-left:max(env(safe-area-inset-left),1.5rem)}.hx-pr-2{padding-right:.5rem}.hx-pr-4{padding-right:1rem}.hx-pr-\[calc\(env\(safe-area-inset-right\)-1\.5rem\)\]{padding-right:calc(env(safe-area-inset-right) - 1.5rem)}.hx-pr-\[max\(env\(safe-area-inset-left\)\,1\.5rem\)\]{padding-right:max(env(safe-area-inset-left),1.5rem)}.hx-pr-\[max\(env\(safe-area-inset-right\)\,1\.5rem\)\]{padding-right:max(env(safe-area-inset-right),1.5rem)}.hx-pt-4{padding-top:1rem}.hx-pt-6{padding-top:1.5rem}.hx-pt-8{padding-top:2rem}.hx-text-left{text-align:left}.hx-text-center{text-align:center}.hx-align-middle{vertical-align:middle}.hx-align-text-bottom{vertical-align:text-bottom}.hx-align-\[-2\.5px\]{vertical-align:-2.5px}.hx-font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,liberation mono,courier new,monospace}.hx-text-2xl{font-size:1.5rem}.hx-text-4xl{font-size:2.25rem}.hx-text-\[\.65rem\]{font-size:.65rem}.hx-text-\[10px\]{font-size:10px}.hx-text-base{font-size:1rem}.hx-text-lg{font-size:1.125rem}.hx-text-sm{font-size:.875rem}.hx-text-xl{font-size:1.25rem}.hx-text-xs{font-size:.75rem}.hx-font-bold{font-weight:700}.hx-font-extrabold{font-weight:800}.hx-font-medium{font-weight:500}.hx-font-normal{font-weight:400}.hx-font-semibold{font-weight:600}.hx-capitalize{text-transform:capitalize}.hx-leading-5{line-height:1.25rem}.hx-leading-6{line-height:1.5rem}.hx-leading-7{line-height:1.75rem}.hx-leading-none{line-height:1}.hx-leading-tight{line-height:1.25}.hx-tracking-tight{letter-spacing:-.015em}.hx-text-\[color\:hsl\(var\(--primary-hue\)\,100\%\,50\%\)\]{--tw-text-opacity:1;color:hsl(var(--primary-hue) 100% 50%/var(--tw-text-opacity))}.hx-text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15/var(--tw-text-opacity))}.hx-text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138/var(--tw-text-opacity))}.hx-text-current{color:currentColor}.hx-text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.hx-text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.hx-text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hx-text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hx-text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hx-text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hx-text-green-900{--tw-text-opacity:1;color:rgb(20 83 45/var(--tw-text-opacity))}.hx-text-indigo-900{--tw-text-opacity:1;color:rgb(49 46 129/var(--tw-text-opacity))}.hx-text-orange-800{--tw-text-opacity:1;color:rgb(154 52 18/var(--tw-text-opacity))}.hx-text-primary-800{--tw-text-opacity:1;color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 32)/var(--tw-text-opacity))}.hx-text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity))}.hx-text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.hx-text-transparent{color:transparent}.hx-text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hx-text-yellow-900{--tw-text-opacity:1;color:rgb(113 63 18/var(--tw-text-opacity))}.hx-underline{text-decoration-line:underline}.hx-no-underline{text-decoration-line:none}.hx-decoration-from-font{text-decoration-thickness:from-font}.hx-underline-offset-2{text-underline-offset:2px}.hx-opacity-0{opacity:0}.hx-opacity-50{opacity:.5}.hx-opacity-80{opacity:.8}.hx-shadow{--tw-shadow:0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hx-shadow-\[0_-12px_16px_\#fff\]{--tw-shadow:0 -12px 16px #fff;--tw-shadow-colored:0 -12px 16px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hx-shadow-\[0_-12px_16px_white\]{--tw-shadow:0 -12px 16px white;--tw-shadow-colored:0 -12px 16px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hx-shadow-\[0_2px_4px_rgba\(0\,0\,0\,\.02\)\,0_1px_0_rgba\(0\,0\,0\,\.06\)\]{--tw-shadow:0 2px 4px rgba(0,0,0,.02),0 1px 0 rgba(0,0,0,.06);--tw-shadow-colored:0 2px 4px var(--tw-shadow-color), 0 1px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hx-shadow-lg{--tw-shadow:0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hx-shadow-sm{--tw-shadow:0 1px 2px 0 rgb(0 0 0 / 0.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hx-shadow-xl{--tw-shadow:0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hx-shadow-gray-100{--tw-shadow-color:#f3f4f6;--tw-shadow:var(--tw-shadow-colored)}.hx-ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.hx-ring-black\/5{--tw-ring-color:rgb(0 0 0 / 0.05)}.hx-transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:150ms}.hx-transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:150ms}.hx-transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:150ms}.hx-transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:150ms}.hx-transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:150ms}.hx-duration-200{transition-duration:200ms}.hx-duration-75{transition-duration:75ms}.hx-ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.\[-webkit-tap-highlight-color\:transparent\]{-webkit-tap-highlight-color:transparent}.\[-webkit-touch-callout\:none\]{-webkit-touch-callout:none}.\[counter-reset\:step\]{counter-reset:step}.\[hyphens\:auto\]{-webkit-hyphens:auto;hyphens:auto}.\[transition\:background-color_1\.5s_ease\]{transition:background-color 1.5s ease}.\[word-break\:break-word\]{word-break:break-word}.content :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5rem;font-size:2.25rem;font-weight:700;letter-spacing:-.015em;--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.content :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity))}.content :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2.5rem;border-bottom-width:1px;border-color:rgb(229 229 229/.7);padding-bottom:.25rem;font-size:1.875rem;font-weight:600;letter-spacing:-.015em;--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}@media(prefers-contrast:more){.content :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-border-opacity:1;border-color:rgb(163 163 163/var(--tw-border-opacity))}}.content :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 44)/.1);--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity))}@media(prefers-contrast:more){.content :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(163 163 163/var(--tw-border-opacity))}}.content :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2rem;font-size:1.5rem;font-weight:600;letter-spacing:-.015em;--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.content :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity))}.content :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2rem;font-size:1.25rem;font-weight:600;letter-spacing:-.015em;--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.content :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity))}.content :where(h5):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2rem;font-size:1.125rem;font-weight:600;letter-spacing:-.015em;--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.content :where(h5):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity))}.content :where(h6):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2rem;font-size:1rem;font-weight:600;letter-spacing:-.015em;--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.content :where(h6):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity))}.content :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.5rem;line-height:1.75rem}.content :where(p):not(:where([class~=not-prose],[class~=not-prose] *)):first-child{margin-top:0}.content :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-text-opacity));text-decoration-line:underline;text-decoration-thickness:from-font;text-underline-position:from-font}.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.5rem;--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity));font-style:italic;--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)):first-child{margin-top:0}.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity));--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir=ltr],[dir=ltr] *){border-left-width:2px;padding-left:1.5rem}.content :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir=rtl],[dir=rtl] *){border-right-width:2px;padding-right:1.5rem}.content :where(pre):not(:where(.hextra-code-block pre,[class~=not-prose],[class~=not-prose] *)){margin-bottom:1rem;overflow-x:auto;border-radius:.75rem;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 39)/.05);padding-top:1rem;padding-bottom:1rem;font-size:.9em;font-weight:500;-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}@media(prefers-contrast:more){.content :where(pre):not(:where(.hextra-code-block pre,[class~=not-prose],[class~=not-prose] *)){border-width:1px;border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 24)/.2);--tw-contrast:contrast(1.5);filter:var(--tw-blur)var(--tw-brightness)var(--tw-contrast)var(--tw-grayscale)var(--tw-hue-rotate)var(--tw-invert)var(--tw-saturate)var(--tw-sepia)var(--tw-drop-shadow)}}.content :where(pre):not(:where(.hextra-code-block pre,[class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 27)/.1)}@media(prefers-contrast:more){.content :where(pre):not(:where(.hextra-code-block pre,[class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 44)/.4)}}.content :where(code):not(:where(.hextra-code-block code,[class~=not-prose],[class~=not-prose] *)){overflow-wrap:break-word;border-radius:.375rem;border-width:1px;border-color:rgb(0 0 0/var(--tw-border-opacity));--tw-border-opacity:0.04;background-color:rgb(0 0 0/var(--tw-bg-opacity));--tw-bg-opacity:0.03;padding-top:.125rem;padding-bottom:.125rem;padding-left:.25em;padding-right:.25em;font-size:.9em}.content :where(code):not(:where(.hextra-code-block code,[class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){border-color:rgb(255 255 255/.1);background-color:rgb(255 255 255/.1)}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)){margin-top:1.5rem;display:block;overflow-x:auto;padding:0}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)):first-child{margin-top:0}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)) tr{margin:0;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity));padding:0}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)) tr:nth-child(even){--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)) tr:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)) tr:is(html[class~=dark] *):nth-child(even){background-color:rgb(75 85 99/.2)}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)) th{margin:0;border-width:1px;--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity));padding-left:1rem;padding-right:1rem;padding-top:.5rem;padding-bottom:.5rem;font-weight:600}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)) th:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)) td{margin:0;border-width:1px;--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity));padding-left:1rem;padding-right:1rem;padding-top:.5rem;padding-bottom:.5rem}.content :where(table):not(:where(.hextra-code-block table,[class~=not-prose],[class~=not-prose] *)) td:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.5rem;list-style-type:decimal}.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)):first-child{margin-top:0}.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir=ltr],[dir=ltr] *){margin-left:1.5rem}.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir=rtl],[dir=rtl] *){margin-right:1.5rem}.content :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)) li{margin-top:.5rem;margin-bottom:.5rem}.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.5rem;list-style-type:disc}.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)):first-child{margin-top:0}.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir=ltr],[dir=ltr] *){margin-left:1.5rem}.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)):where([dir=rtl],[dir=rtl] *){margin-right:1.5rem}.content :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)) li{margin-top:.5rem;margin-bottom:.5rem}.content :where(ul,ol)>li>:where(ul,ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.content :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){overflow-wrap:break-word;border-radius:.375rem;border-width:1px;border-color:rgb(0 0 0/var(--tw-border-opacity));--tw-border-opacity:0.04;background-color:rgb(0 0 0/var(--tw-bg-opacity));--tw-bg-opacity:0.03;padding-top:.125rem;padding-bottom:.125rem;padding-left:.25em;padding-right:.25em;font-size:.9em}.content :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){border-color:rgb(255 255 255/.1);background-color:rgb(255 255 255/.1)}.content :where(pre.mermaid):not(:where(.hextra-code-block pre,[class~=not-prose],[class~=not-prose] *)){border-radius:0;background-color:transparent}.content :where(pre.mermaid):not(:where(.hextra-code-block pre,[class~=not-prose],[class~=not-prose] *)):is(html[class~=dark] *){background-color:transparent}.content :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-left:auto;margin-right:auto;margin-top:1rem;margin-bottom:1rem;border-radius:.375rem}.content :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)) figcaption{margin-top:.5rem;display:block;text-align:center;font-size:.875rem;--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.content :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)) figcaption:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.content :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)) dt{margin-top:1.5rem;font-weight:600}.content :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)) dd{margin-top:.5rem;margin-bottom:.5rem;padding-inline-start:1.5rem}.content .footnotes{margin-top:3rem;font-size:.875rem}.subheading-anchor{opacity:0;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:150ms}.subheading-anchor:where([dir=ltr],[dir=ltr] *){margin-left:.25rem}.subheading-anchor:where([dir=rtl],[dir=rtl] *){margin-right:.25rem}span:target+.subheading-anchor,:hover>.subheading-anchor,.subheading-anchor:focus{opacity:1}span+.subheading-anchor,:hover>.subheading-anchor{text-decoration-line:none !important}.subheading-anchor:after{padding-left:.25rem;padding-right:.25rem;--tw-content:'#';content:var(--tw-content);--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.subheading-anchor:is(html[class~=dark] *):after{--tw-text-opacity:1;color:rgb(64 64 64/var(--tw-text-opacity))}span:target+.subheading-anchor:after{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}span:target+.subheading-anchor:is(html[class~=dark] *):after{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity))}article details>summary::-webkit-details-marker{display:none}article details>summary::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='hx-h-5 hx-w-5' viewBox='0 0 20 20' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd' /%3E%3C/svg%3E");height:1.2em;width:1.2em;vertical-align:-4px;padding:0 .6em}:lang(fa) ol{list-style-type:persian}.highlight{}.highlight .chroma .err{color:#a61717;background-color:#e3d2d2}.highlight .chroma .lnlinks{outline:none;text-decoration:none;color:inherit}.highlight .chroma .line{display:flex}.highlight .chroma .k{color:#000;font-weight:700}.highlight .chroma .kc{color:#000;font-weight:700}.highlight .chroma .kd{color:#000;font-weight:700}.highlight .chroma .kn{color:#000;font-weight:700}.highlight .chroma .kp{color:#000;font-weight:700}.highlight .chroma .kr{color:#000;font-weight:700}.highlight .chroma .kt{color:#458;font-weight:700}.highlight .chroma .na{color:teal}.highlight .chroma .nb{color:#0086b3}.highlight .chroma .bp{color:#999}.highlight .chroma .nc{color:#458;font-weight:700}.highlight .chroma .no{color:teal}.highlight .chroma .nd{color:#3c5d5d;font-weight:700}.highlight .chroma .ni{color:purple}.highlight .chroma .ne{color:#900;font-weight:700}.highlight .chroma .nf{color:#900;font-weight:700}.highlight .chroma .nl{color:#900;font-weight:700}.highlight .chroma .nn{color:#555}.highlight .chroma .nt{color:navy}.highlight .chroma .nv{color:teal}.highlight .chroma .vc{color:teal}.highlight .chroma .vg{color:teal}.highlight .chroma .vi{color:teal}.highlight .chroma .s{color:#d14}.highlight .chroma .sa{color:#d14}.highlight .chroma .sb{color:#d14}.highlight .chroma .sc{color:#d14}.highlight .chroma .dl{color:#d14}.highlight .chroma .sd{color:#d14}.highlight .chroma .s2{color:#d14}.highlight .chroma .se{color:#d14}.highlight .chroma .sh{color:#d14}.highlight .chroma .si{color:#d14}.highlight .chroma .sx{color:#d14}.highlight .chroma .sr{color:#009926}.highlight .chroma .s1{color:#d14}.highlight .chroma .ss{color:#990073}.highlight .chroma .m{color:#099}.highlight .chroma .mb{color:#099}.highlight .chroma .mf{color:#099}.highlight .chroma .mh{color:#099}.highlight .chroma .mi{color:#099}.highlight .chroma .il{color:#099}.highlight .chroma .mo{color:#099}.highlight .chroma .o{color:#000;font-weight:700}.highlight .chroma .ow{color:#000;font-weight:700}.highlight .chroma .c{color:#998;font-style:italic}.highlight .chroma .ch{color:#998;font-style:italic}.highlight .chroma .cm{color:#998;font-style:italic}.highlight .chroma .c1{color:#998;font-style:italic}.highlight .chroma .cs{color:#999;font-weight:700;font-style:italic}.highlight .chroma .cp{color:#999;font-weight:700;font-style:italic}.highlight .chroma .cpf{color:#999;font-weight:700;font-style:italic}.highlight .chroma .gd{color:#000;background-color:#fdd}.highlight .chroma .ge{color:#000;font-style:italic}.highlight .chroma .gr{color:#a00}.highlight .chroma .gh{color:#999}.highlight .chroma .gi{color:#000;background-color:#dfd}.highlight .chroma .go{color:#888}.highlight .chroma .gp{color:#555}.highlight .chroma .gs{font-weight:700}.highlight .chroma .gu{color:#aaa}.highlight .chroma .gt{color:#a00}.highlight .chroma .gl{text-decoration:underline}.highlight .chroma .w{color:#bbb}.dark .highlight{}.dark .highlight .chroma .x{}.dark .highlight .chroma .err{color:#f85149}.dark .highlight .chroma .cl{}.dark .highlight .chroma .lnlinks{outline:none;text-decoration:none;color:inherit}.dark .highlight .chroma .line{display:flex}.dark .highlight .chroma .k{color:#ff7b72}.dark .highlight .chroma .kc{color:#79c0ff}.dark .highlight .chroma .kd{color:#ff7b72}.dark .highlight .chroma .kn{color:#ff7b72}.dark .highlight .chroma .kp{color:#79c0ff}.dark .highlight .chroma .kr{color:#ff7b72}.dark .highlight .chroma .kt{color:#ff7b72}.dark .highlight .chroma .n{}.dark .highlight .chroma .na{}.dark .highlight .chroma .nb{}.dark .highlight .chroma .bp{}.dark .highlight .chroma .nc{color:#f0883e;font-weight:700}.dark .highlight .chroma .no{color:#79c0ff;font-weight:700}.dark .highlight .chroma .nd{color:#d2a8ff;font-weight:700}.dark .highlight .chroma .ni{color:#ffa657}.dark .highlight .chroma .ne{color:#f0883e;font-weight:700}.dark .highlight .chroma .nf{color:#d2a8ff;font-weight:700}.dark .highlight .chroma .fm{}.dark .highlight .chroma .nl{color:#79c0ff;font-weight:700}.dark .highlight .chroma .nn{color:#ff7b72}.dark .highlight .chroma .nx{}.dark .highlight .chroma .py{color:#79c0ff}.dark .highlight .chroma .nt{color:#7ee787}.dark .highlight .chroma .nv{color:#79c0ff}.dark .highlight .chroma .vc{}.dark .highlight .chroma .vg{}.dark .highlight .chroma .vi{}.dark .highlight .chroma .vm{}.dark .highlight .chroma .l{color:#a5d6ff}.dark .highlight .chroma .ld{color:#79c0ff}.dark .highlight .chroma .s{color:#a5d6ff}.dark .highlight .chroma .sa{color:#79c0ff}.dark .highlight .chroma .sb{color:#a5d6ff}.dark .highlight .chroma .sc{color:#a5d6ff}.dark .highlight .chroma .dl{color:#79c0ff}.dark .highlight .chroma .sd{color:#a5d6ff}.dark .highlight .chroma .s2{color:#a5d6ff}.dark .highlight .chroma .se{color:#79c0ff}.dark .highlight .chroma .sh{color:#79c0ff}.dark .highlight .chroma .si{color:#a5d6ff}.dark .highlight .chroma .sx{color:#a5d6ff}.dark .highlight .chroma .sr{color:#79c0ff}.dark .highlight .chroma .s1{color:#a5d6ff}.dark .highlight .chroma .ss{color:#a5d6ff}.dark .highlight .chroma .m{color:#a5d6ff}.dark .highlight .chroma .mb{color:#a5d6ff}.dark .highlight .chroma .mf{color:#a5d6ff}.dark .highlight .chroma .mh{color:#a5d6ff}.dark .highlight .chroma .mi{color:#a5d6ff}.dark .highlight .chroma .il{color:#a5d6ff}.dark .highlight .chroma .mo{color:#a5d6ff}.dark .highlight .chroma .o{color:#ff7b72;font-weight:700}.dark .highlight .chroma .ow{color:#ff7b72;font-weight:700}.dark .highlight .chroma .p{}.dark .highlight .chroma .c{color:#8b949e;font-style:italic}.dark .highlight .chroma .ch{color:#8b949e;font-style:italic}.dark .highlight .chroma .cm{color:#8b949e;font-style:italic}.dark .highlight .chroma .c1{color:#8b949e;font-style:italic}.dark .highlight .chroma .cs{color:#8b949e;font-weight:700;font-style:italic}.dark .highlight .chroma .cp{color:#8b949e;font-weight:700;font-style:italic}.dark .highlight .chroma .cpf{color:#8b949e;font-weight:700;font-style:italic}.dark .highlight .chroma .g{}.dark .highlight .chroma .gd{color:#ffa198;background-color:#490202}.dark .highlight .chroma .ge{color:inherit;font-style:italic}.dark .highlight .chroma .gr{color:#ffa198}.dark .highlight .chroma .gh{color:#79c0ff;font-weight:700}.dark .highlight .chroma .gi{color:#56d364;background-color:#0f5323}.dark .highlight .chroma .go{color:#8b949e}.dark .highlight .chroma .gp{color:#8b949e}.dark .highlight .chroma .gs{font-weight:700}.dark .highlight .chroma .gu{color:#79c0ff}.dark .highlight .chroma .gt{color:#ff7b72}.dark .highlight .chroma .gl{text-decoration:underline}.dark .highlight .chroma .w{color:#6e7681}.hextra-code-block{font-size:.9em;line-height:1.25rem}.hextra-code-block pre{overflow-x:auto;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 39)/.05);font-size:.9em;font-weight:500;-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}@media(prefers-contrast:more){.hextra-code-block pre{border-width:1px;border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 24)/.2);--tw-contrast:contrast(1.5);filter:var(--tw-blur)var(--tw-brightness)var(--tw-contrast)var(--tw-grayscale)var(--tw-hue-rotate)var(--tw-invert)var(--tw-saturate)var(--tw-sepia)var(--tw-drop-shadow)}}.hextra-code-block pre:is(html[class~=dark] *){background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 27)/.1)}@media(prefers-contrast:more){.hextra-code-block pre:is(html[class~=dark] *){border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 44)/.4)}}.hextra-code-block .filename{position:absolute;top:0;z-index:1;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-top-left-radius:.75rem;border-top-right-radius:.75rem;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 39)/.05);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;padding-right:1rem;font-size:.75rem;--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hextra-code-block .filename:is(html[class~=dark] *){background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 27)/.1);--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.hextra-code-block .filename+pre:not(.lntable pre){padding-top:3rem}.hextra-code-block pre:not(.lntable pre){margin-bottom:1rem;border-radius:.75rem;padding-left:1rem;padding-right:1rem;padding-top:1rem;padding-bottom:1rem}.hextra-code-block div:nth-of-type(2) pre{padding-top:3rem;padding-bottom:1rem}.chroma .lntable{margin:0;display:block;width:auto;overflow:auto;border-radius:.75rem}.chroma .lntable pre{padding-top:1rem;padding-bottom:1rem}.chroma .ln,.chroma .lnt:not(.hl>.lnt),.chroma .hl:not(.line){min-width:2.6rem;padding-left:1rem;padding-right:1rem;--tw-text-opacity:1;color:rgb(82 82 82/var(--tw-text-opacity))}.chroma .ln:is(html[class~=dark] *),.chroma .lnt:not(.hl>.lnt):is(html[class~=dark] *),.chroma .hl:not(.line):is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(212 212 212/var(--tw-text-opacity))}.chroma .lntd{padding:0;vertical-align:top}.chroma .lntd:last-of-type{width:100%}.chroma .hl{display:block;width:100%;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 32)/.1)}.hextra-cards{grid-template-columns:repeat(auto-fill,minmax(max(250px,calc((100% - 1rem * 2)/var(--hextra-cards-grid-cols))),1fr))}.hextra-card{position:relative}.hextra-card img{-webkit-user-select:none;-moz-user-select:none;user-select:none}.hextra-card:hover svg{color:currentColor}.hextra-card svg{width:1.5rem;color:#0003;transition:color .3s ease}.hextra-card p{margin-top:.5rem;position:relative}.dark .hextra-card svg{color:#fff6}.dark .hextra-card:hover svg{color:currentColor}.hextra-card-tag{position:absolute;top:5px;right:5px;z-index:10}.steps h3{counter-increment:step}.steps h3:before{position:absolute;height:33px;width:33px;border-width:4px;--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.steps h3:is(html[class~=dark] *):before{--tw-border-opacity:1;border-color:rgb(17 17 17/var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.steps h3:before{border-radius:9999px;text-align:center;text-indent:-1px;font-size:1rem;font-weight:400;--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity));margin-top:3px}.steps h3:where([dir=ltr],[dir=ltr] *):before{margin-left:-41px}.steps h3:where([dir=rtl],[dir=rtl] *):before{margin-right:-44px}.steps h3:before{content:counter(step)}:lang(fa) .steps h3:before{content:counter(step,persian)}.search-wrapper li{margin-left:.625rem;margin-right:.625rem;overflow-wrap:break-word;border-radius:.375rem;--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}@media(prefers-contrast:more){.search-wrapper li{border-width:1px;border-color:transparent}}.search-wrapper li:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.search-wrapper li a{display:block;scroll-margin:3rem;padding-left:.625rem;padding-right:.625rem;padding-top:.5rem;padding-bottom:.5rem}.search-wrapper li .title{font-size:1rem;font-weight:600;line-height:1.25rem}.search-wrapper li .active{border-radius:.375rem;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 50)/.1)}@media(prefers-contrast:more){.search-wrapper li .active{--tw-border-opacity:1;border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 50)/var(--tw-border-opacity))}}.search-wrapper .no-result{display:block;-webkit-user-select:none;-moz-user-select:none;user-select:none;padding:2rem;text-align:center;font-size:.875rem;--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.search-wrapper .prefix{margin-left:.625rem;margin-right:.625rem;margin-bottom:.5rem;margin-top:1.5rem;-webkit-user-select:none;-moz-user-select:none;user-select:none;border-bottom-width:1px;border-color:rgb(0 0 0/.1);padding-left:.625rem;padding-right:.625rem;padding-bottom:.375rem;font-size:.75rem;font-weight:600;text-transform:uppercase;--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.search-wrapper .prefix:first-child{margin-top:0}@media(prefers-contrast:more){.search-wrapper .prefix{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity));--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}}.search-wrapper .prefix:is(html[class~=dark] *){border-color:rgb(255 255 255/.2);--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}@media(prefers-contrast:more){.search-wrapper .prefix:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(249 250 251/var(--tw-border-opacity));--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}}.search-wrapper .excerpt{margin-top:.25rem;overflow:hidden;text-overflow:ellipsis;font-size:.875rem;line-height:1.35rem;--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.search-wrapper .excerpt:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}@media(prefers-contrast:more){.search-wrapper .excerpt:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}}.search-wrapper .excerpt{display:-webkit-box;line-clamp:1;-webkit-line-clamp:1;-webkit-box-orient:vertical}.search-wrapper .match{--tw-text-opacity:1;color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-text-opacity))}@media(max-width:767px){.sidebar-container{position:fixed;top:0;bottom:0;z-index:15;width:100%;overscroll-behavior:contain;--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));padding-top:calc(var(--navbar-height))}.sidebar-container:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(17 17 17/var(--tw-bg-opacity))}.sidebar-container{transition:transform .8s cubic-bezier(.52,.16,.04,1);will-change:transform,opacity;contain:layout style;backface-visibility:hidden}}.sidebar-container li>div{height:0}.sidebar-container li.open>div{height:auto;padding-top:.25rem}.sidebar-container li.open>a>span>svg>path{--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y))rotate(var(--tw-rotate))skewX(var(--tw-skew-x))skewY(var(--tw-skew-y))scaleX(var(--tw-scale-x))scaleY(var(--tw-scale-y))}nav .search-wrapper{display:none}@media(min-width:768px){nav .search-wrapper{display:inline-block}}@supports(((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px)))){.nav-container-blur{background-color:rgb(255 255 255/.85);--tw-backdrop-blur:blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur)var(--tw-backdrop-brightness)var(--tw-backdrop-contrast)var(--tw-backdrop-grayscale)var(--tw-backdrop-hue-rotate)var(--tw-backdrop-invert)var(--tw-backdrop-opacity)var(--tw-backdrop-saturate)var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur)var(--tw-backdrop-brightness)var(--tw-backdrop-contrast)var(--tw-backdrop-grayscale)var(--tw-backdrop-hue-rotate)var(--tw-backdrop-invert)var(--tw-backdrop-opacity)var(--tw-backdrop-saturate)var(--tw-backdrop-sepia)}.nav-container-blur:is(html[class~=dark] *){background-color:rgb(17 17 17/.8) !important}}.hamburger-menu svg g{transform-origin:center;transition:transform .2s cubic-bezier(.25,1,.5,1)}.hamburger-menu svg path{opacity:1;transition:transform .2s cubic-bezier(.25,1,.5,1).2s,opacity .2s ease .2s}.hamburger-menu svg.open path{transition:transform .2s cubic-bezier(.25,1,.5,1),opacity 0s ease .2s}.hamburger-menu svg.open g{transition:transform .2s cubic-bezier(.25,1,.5,1).2s}.hamburger-menu svg.open>path{opacity:0}.hamburger-menu svg.open>g:nth-of-type(1){--tw-rotate:45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y))rotate(var(--tw-rotate))skewX(var(--tw-skew-x))skewY(var(--tw-skew-y))scaleX(var(--tw-scale-x))scaleY(var(--tw-scale-y))}.hamburger-menu svg.open>g:nth-of-type(1) path{transform:translate3d(0,4px,0)}.hamburger-menu svg.open>g:nth-of-type(2){--tw-rotate:-45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y))rotate(var(--tw-rotate))skewX(var(--tw-skew-x))skewY(var(--tw-skew-y))scaleX(var(--tw-scale-x))scaleY(var(--tw-scale-y))}.hamburger-menu svg.open>g:nth-of-type(2) path{transform:translate3d(0,-4px,0)}.hextra-scrollbar,.hextra-scrollbar *{scrollbar-width:thin;scrollbar-color:oklch(55.55% 0 0/40%)transparent;scrollbar-gutter:stable}.hextra-scrollbar::-webkit-scrollbar,.hextra-scrollbar *::-webkit-scrollbar{height:.75rem;width:.75rem}.hextra-scrollbar::-webkit-scrollbar-track,.hextra-scrollbar *::-webkit-scrollbar-track{background-color:transparent}.hextra-scrollbar::-webkit-scrollbar-thumb,.hextra-scrollbar *::-webkit-scrollbar-thumb{border-radius:10px}.hextra-scrollbar:hover::-webkit-scrollbar-thumb,.hextra-scrollbar *:hover::-webkit-scrollbar-thumb{border:3px solid transparent;background-color:var(--tw-shadow-color);background-clip:content-box;--tw-shadow-color:rgb(115 115 115 / 0.2);--tw-shadow:var(--tw-shadow-colored)}.hextra-scrollbar:hover::-webkit-scrollbar-thumb:hover,.hextra-scrollbar *:hover::-webkit-scrollbar-thumb:hover{--tw-shadow-color:rgb(115 115 115 / 0.4);--tw-shadow:var(--tw-shadow-colored)}@supports(((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px)))){.hextra-code-copy-btn{--tw-bg-opacity:.85;--tw-backdrop-blur:blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur)var(--tw-backdrop-brightness)var(--tw-backdrop-contrast)var(--tw-backdrop-grayscale)var(--tw-backdrop-hue-rotate)var(--tw-backdrop-invert)var(--tw-backdrop-opacity)var(--tw-backdrop-saturate)var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur)var(--tw-backdrop-brightness)var(--tw-backdrop-contrast)var(--tw-backdrop-grayscale)var(--tw-backdrop-hue-rotate)var(--tw-backdrop-invert)var(--tw-backdrop-opacity)var(--tw-backdrop-saturate)var(--tw-backdrop-sepia)}.hextra-code-copy-btn:is(html[class~=dark] *){--tw-bg-opacity:0.8}}@media(min-width:1024px){.hextra-feature-grid{grid-template-columns:repeat(var(--hextra-feature-grid-cols),minmax(0,1fr))}}.hextra-jupyter-code-cell{scrollbar-gutter:auto;margin-top:1.5rem}.hextra-jupyter-code-cell .hextra-jupyter-code-cell-outputs-container{overflow:hidden;font-size:.75rem}.hextra-jupyter-code-cell .hextra-jupyter-code-cell-outputs-container .hextra-jupyter-code-cell-outputs{max-height:50vh;overflow:auto}.hextra-jupyter-code-cell .hextra-jupyter-code-cell-outputs-container .hextra-jupyter-code-cell-outputs pre{max-width:100%;overflow:auto;font-size:.75rem}.hextra-badge{display:inline-flex;align-items:center}html{font-size:1rem;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-feature-settings:"rlig" 1,"calt" 1,"ss01" 1;-webkit-tap-highlight-color:transparent}body{width:100%;--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}body:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(17 17 17/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:root{--primary-hue:212deg;--primary-saturation:100%;--primary-lightness:50%;--navbar-height:4rem;--menu-height:3.75rem}.dark{--primary-hue:204deg;--primary-saturation:100%;--primary-lightness:50%}.placeholder\:hx-text-gray-500::-moz-placeholder{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.placeholder\:hx-text-gray-500::placeholder{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.before\:hx-pointer-events-none::before{content:var(--tw-content);pointer-events:none}.before\:hx-absolute::before{content:var(--tw-content);position:absolute}.before\:hx-inset-0::before{content:var(--tw-content);inset:0}.before\:hx-inset-y-1::before{content:var(--tw-content);top:.25rem;bottom:.25rem}.before\:hx-mr-1::before{content:var(--tw-content);margin-right:.25rem}.before\:hx-inline-block::before{content:var(--tw-content);display:inline-block}.before\:hx-w-px::before{content:var(--tw-content);width:1px}.before\:hx-bg-gray-200::before{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.before\:hx-opacity-25::before{content:var(--tw-content);opacity:.25}.before\:hx-transition-transform::before{content:var(--tw-content);transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:150ms}.before\:hx-content-\[\'\#\'\]::before{--tw-content:'#';content:var(--tw-content)}.before\:hx-content-\[\'\'\]::before{--tw-content:'';content:var(--tw-content)}.before\:hx-content-\[\\\"\\\"\]::before{--tw-content:\"\";content:var(--tw-content)}.first\:hx-mt-0:first-child{margin-top:0}.last-of-type\:hx-mb-0:last-of-type{margin-bottom:0}.hover\:hx-border-gray-200:hover{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.hover\:hx-border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:hx-border-gray-400:hover{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.hover\:hx-border-gray-900:hover{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}.hover\:hx-bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:hx-bg-gray-800\/5:hover{background-color:rgb(31 41 55/5%)}.hover\:hx-bg-primary-50:hover{--tw-bg-opacity:1;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 47)/var(--tw-bg-opacity))}.hover\:hx-bg-primary-700:hover{--tw-bg-opacity:1;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 39)/var(--tw-bg-opacity))}.hover\:hx-bg-slate-50:hover{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity))}.hover\:hx-text-black:hover{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.hover\:hx-text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:hx-text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:hx-text-primary-600:hover{--tw-text-opacity:1;color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-text-opacity))}.hover\:hx-opacity-60:hover{opacity:.6}.hover\:hx-opacity-75:hover{opacity:.75}.hover\:hx-shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:hx-shadow-md:hover{--tw-shadow:0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:hx-shadow-gray-100:hover{--tw-shadow-color:#f3f4f6;--tw-shadow:var(--tw-shadow-colored)}.focus\:hx-bg-white:focus{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.focus\:hx-outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:hx-ring-4:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:hx-ring-primary-300:focus{--tw-ring-opacity:1;--tw-ring-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 27) / var(--tw-ring-opacity))}.active\:hx-bg-gray-400\/20:active{background-color:rgb(156 163 175/.2)}.active\:hx-opacity-50:active{opacity:.5}.active\:hx-shadow-sm:active{--tw-shadow:0 1px 2px 0 rgb(0 0 0 / 0.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.active\:hx-shadow-gray-200:active{--tw-shadow-color:#e5e7eb;--tw-shadow:var(--tw-shadow-colored)}.hx-group[open] .group-open\:before\:hx-rotate-90::before{content:var(--tw-content);--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y))rotate(var(--tw-rotate))skewX(var(--tw-skew-x))skewY(var(--tw-skew-y))scaleX(var(--tw-scale-x))scaleY(var(--tw-scale-y))}.hx-group:hover .group-hover\:hx-underline{text-decoration-line:underline}.hx-group\/code:hover .group-hover\/code\:hx-opacity-100{opacity:1}.hx-group\/copybtn.copied .group-\[\.copied\]\/copybtn\:hx-block{display:block}.hx-group\/copybtn.copied .group-\[\.copied\]\/copybtn\:hx-hidden{display:none}.data-\[state\=selected\]\:hx-block[data-state=selected]{display:block}.data-\[state\=closed\]\:hx-hidden[data-state=closed]{display:none}.data-\[state\=open\]\:hx-hidden[data-state=open]{display:none}.data-\[state\=selected\]\:hx-border-primary-500[data-state=selected]{--tw-border-opacity:1;border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 50)/var(--tw-border-opacity))}.data-\[state\=selected\]\:hx-text-primary-600[data-state=selected]{--tw-text-opacity:1;color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-text-opacity))}.hx-group[data-theme=dark] .group-data-\[theme\=dark\]\:hx-hidden{display:none}.hx-group[data-theme=light] .group-data-\[theme\=light\]\:hx-hidden{display:none}@media(prefers-contrast:more){.contrast-more\:hx-border{border-width:1px}.contrast-more\:hx-border-t{border-top-width:1px}.contrast-more\:hx-border-current{border-color:initial}.contrast-more\:hx-border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity))}.contrast-more\:hx-border-gray-900{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}.contrast-more\:hx-border-neutral-400{--tw-border-opacity:1;border-color:rgb(163 163 163/var(--tw-border-opacity))}.contrast-more\:hx-border-primary-500{--tw-border-opacity:1;border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 50)/var(--tw-border-opacity))}.contrast-more\:hx-border-transparent{border-color:transparent}.contrast-more\:hx-font-bold{font-weight:700}.contrast-more\:hx-text-current{color:currentColor}.contrast-more\:hx-text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.contrast-more\:hx-text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.contrast-more\:hx-text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.contrast-more\:hx-underline{text-decoration-line:underline}.contrast-more\:hx-shadow-\[0_0_0_1px_\#000\]{--tw-shadow:0 0 0 1px #000;--tw-shadow-colored:0 0 0 1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.contrast-more\:hx-shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.contrast-more\:hover\:hx-border-gray-900:hover{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}}.dark\:hx-block:is(html[class~=dark] *){display:block}.dark\:hx-hidden:is(html[class~=dark] *){display:none}.dark\:hx-border-amber-200\/30:is(html[class~=dark] *){border-color:rgb(253 230 138/.3)}.dark\:hx-border-blue-200\/30:is(html[class~=dark] *){border-color:rgb(191 219 254/.3)}.dark\:hx-border-gray-100\/20:is(html[class~=dark] *){border-color:rgb(243 244 246/.2)}.dark\:hx-border-gray-400:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.dark\:hx-border-green-200\/30:is(html[class~=dark] *){border-color:rgb(187 247 208/.3)}.dark\:hx-border-indigo-200\/30:is(html[class~=dark] *){border-color:rgb(199 210 254/.3)}.dark\:hx-border-neutral-700:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(64 64 64/var(--tw-border-opacity))}.dark\:hx-border-neutral-800:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(38 38 38/var(--tw-border-opacity))}.dark\:hx-border-orange-400\/30:is(html[class~=dark] *){border-color:rgb(251 146 60/.3)}.dark\:hx-border-red-200\/30:is(html[class~=dark] *){border-color:rgb(254 202 202/.3)}.dark\:hx-border-white\/10:is(html[class~=dark] *){border-color:rgb(255 255 255/.1)}.dark\:hx-border-yellow-200\/30:is(html[class~=dark] *){border-color:rgb(254 240 138/.3)}.dark\:hx-bg-amber-900\/30:is(html[class~=dark] *){background-color:rgb(120 53 15/.3)}.dark\:hx-bg-black\/60:is(html[class~=dark] *){background-color:rgb(0 0 0/.6)}.dark\:hx-bg-blue-900\/30:is(html[class~=dark] *){background-color:rgb(30 58 138/.3)}.dark\:hx-bg-dark:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(17 17 17/var(--tw-bg-opacity))}.dark\:hx-bg-dark\/50:is(html[class~=dark] *){background-color:rgb(17 17 17/.5)}.dark\:hx-bg-gray-50\/10:is(html[class~=dark] *){background-color:rgb(249 250 251/.1)}.dark\:hx-bg-green-900\/30:is(html[class~=dark] *){background-color:rgb(20 83 45/.3)}.dark\:hx-bg-indigo-900\/30:is(html[class~=dark] *){background-color:rgb(49 46 129/.3)}.dark\:hx-bg-neutral-800:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.dark\:hx-bg-neutral-900:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.dark\:hx-bg-orange-400\/20:is(html[class~=dark] *){background-color:rgb(251 146 60/.2)}.dark\:hx-bg-primary-300\/10:is(html[class~=dark] *){background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 27)/.1)}.dark\:hx-bg-primary-400\/10:is(html[class~=dark] *){background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 16)/.1)}.dark\:hx-bg-primary-600:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-bg-opacity))}.dark\:hx-bg-red-900\/30:is(html[class~=dark] *){background-color:rgb(127 29 29/.3)}.dark\:hx-bg-yellow-700\/30:is(html[class~=dark] *){background-color:rgb(161 98 7/.3)}.dark\:hx-from-gray-100:is(html[class~=dark] *){--tw-gradient-from:#f3f4f6 var(--tw-gradient-from-position);--tw-gradient-to:rgb(243 244 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.dark\:hx-to-gray-400:is(html[class~=dark] *){--tw-gradient-to:#9ca3af var(--tw-gradient-to-position)}.dark\:hx-text-amber-200:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity))}.dark\:hx-text-blue-200:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity))}.dark\:hx-text-gray-100:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.dark\:hx-text-gray-200:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark\:hx-text-gray-300:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark\:hx-text-gray-400:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:hx-text-gray-50:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}.dark\:hx-text-green-200:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity))}.dark\:hx-text-indigo-200:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity))}.dark\:hx-text-neutral-200:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(229 229 229/var(--tw-text-opacity))}.dark\:hx-text-neutral-400:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))}.dark\:hx-text-orange-300:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity))}.dark\:hx-text-primary-600:is(html[class~=dark] *){--tw-text-opacity:1;color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-text-opacity))}.dark\:hx-text-red-200:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity))}.dark\:hx-text-slate-100:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity))}.dark\:hx-text-yellow-200:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity))}.dark\:hx-opacity-80:is(html[class~=dark] *){opacity:.8}.dark\:hx-shadow-\[0_-12px_16px_\#111\]:is(html[class~=dark] *){--tw-shadow:0 -12px 16px #111;--tw-shadow-colored:0 -12px 16px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.dark\:hx-shadow-\[0_-1px_0_rgba\(255\2c 255\2c 255\2c \.1\)_inset\]:is(html[class~=dark] *){--tw-shadow:0 -1px 0 rgba(255,255,255,.1) inset;--tw-shadow-colored:inset 0 -1px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.dark\:hx-shadow-none:is(html[class~=dark] *){--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.dark\:hx-ring-white\/20:is(html[class~=dark] *){--tw-ring-color:rgb(255 255 255 / 0.2)}.dark\:placeholder\:hx-text-gray-400:is(html[class~=dark] *)::-moz-placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:placeholder\:hx-text-gray-400:is(html[class~=dark] *)::placeholder{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:before\:hx-bg-neutral-800:is(html[class~=dark] *)::before{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.dark\:before\:hx-invert:is(html[class~=dark] *)::before{content:var(--tw-content);--tw-invert:invert(100%);filter:var(--tw-blur)var(--tw-brightness)var(--tw-contrast)var(--tw-grayscale)var(--tw-hue-rotate)var(--tw-invert)var(--tw-saturate)var(--tw-sepia)var(--tw-drop-shadow)}.dark\:hover\:hx-border-gray-100:hover:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.dark\:hover\:hx-border-gray-600:hover:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.dark\:hover\:hx-border-neutral-500:hover:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(115 115 115/var(--tw-border-opacity))}.dark\:hover\:hx-border-neutral-700:hover:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(64 64 64/var(--tw-border-opacity))}.dark\:hover\:hx-border-neutral-800:hover:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(38 38 38/var(--tw-border-opacity))}.dark\:hover\:hx-bg-gray-100\/5:hover:is(html[class~=dark] *){background-color:rgb(243 244 246/5%)}.dark\:hover\:hx-bg-neutral-700:hover:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.dark\:hover\:hx-bg-neutral-800:hover:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.dark\:hover\:hx-bg-neutral-900:hover:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.dark\:hover\:hx-bg-primary-100\/5:hover:is(html[class~=dark] *){background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness))/50) * 44)/.05)}.dark\:hover\:hx-bg-primary-700:hover:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 39)/var(--tw-bg-opacity))}.hover\:dark\:hx-bg-primary-500\/10:is(html[class~=dark] *):hover{background-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 50)/.1)}.dark\:hover\:hx-text-gray-100:hover:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.dark\:hover\:hx-text-gray-200:hover:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark\:hover\:hx-text-gray-300:hover:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark\:hover\:hx-text-gray-50:hover:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}.dark\:hover\:hx-text-neutral-50:hover:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(250 250 250/var(--tw-text-opacity))}.dark\:hover\:hx-text-white:hover:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:dark\:hx-text-primary-600:is(html[class~=dark] *):hover{--tw-text-opacity:1;color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-text-opacity))}.dark\:hover\:hx-shadow-none:hover:is(html[class~=dark] *){--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.dark\:focus\:hx-bg-dark:focus:is(html[class~=dark] *){--tw-bg-opacity:1;background-color:rgb(17 17 17/var(--tw-bg-opacity))}.dark\:focus\:hx-ring-primary-800:focus:is(html[class~=dark] *){--tw-ring-opacity:1;--tw-ring-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 32) / var(--tw-ring-opacity))}.data-\[state\=selected\]\:dark\:hx-border-primary-500:is(html[class~=dark] *)[data-state=selected]{--tw-border-opacity:1;border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 50)/var(--tw-border-opacity))}.data-\[state\=selected\]\:dark\:hx-text-primary-600:is(html[class~=dark] *)[data-state=selected]{--tw-text-opacity:1;color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 45)/var(--tw-text-opacity))}@media(prefers-contrast:more){.contrast-more\:dark\:hx-border-current:is(html[class~=dark] *){border-color:initial}.contrast-more\:dark\:hx-border-gray-50:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(249 250 251/var(--tw-border-opacity))}.contrast-more\:dark\:hx-border-neutral-400:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(163 163 163/var(--tw-border-opacity))}.contrast-more\:dark\:hx-border-primary-500:is(html[class~=dark] *){--tw-border-opacity:1;border-color:hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness)/50) * 50)/var(--tw-border-opacity))}.dark\:contrast-more\:hx-border-neutral-400:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(163 163 163/var(--tw-border-opacity))}.contrast-more\:dark\:hx-text-current:is(html[class~=dark] *){color:currentColor}.contrast-more\:dark\:hx-text-gray-100:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.contrast-more\:dark\:hx-text-gray-300:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.contrast-more\:dark\:hx-text-gray-50:is(html[class~=dark] *){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}.contrast-more\:dark\:hx-shadow-\[0_0_0_1px_\#fff\]:is(html[class~=dark] *){--tw-shadow:0 0 0 1px #fff;--tw-shadow-colored:0 0 0 1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.contrast-more\:dark\:hx-shadow-none:is(html[class~=dark] *){--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.contrast-more\:dark\:hover\:hx-border-gray-50:hover:is(html[class~=dark] *){--tw-border-opacity:1;border-color:rgb(249 250 251/var(--tw-border-opacity))}}@media not all and (min-width:1280px){.max-xl\:hx-hidden{display:none}}@media not all and (min-width:1024px){.max-lg\:hx-min-h-\[340px\]{min-height:340px}}@media not all and (min-width:768px){.max-md\:hx-hidden{display:none}.max-md\:hx-min-h-\[340px\]{min-height:340px}.max-md\:\[transform\:translate3d\(0\2c -100\%\2c 0\)\]{transform:translate3d(0,-100%,0)}}@media not all and (min-width:640px){.max-sm\:hx-grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}}@media(min-width:640px){.sm\:hx-block{display:block}.sm\:hx-flex{display:flex}.sm\:hx-w-\[110\%\]{width:110%}.sm\:hx-items-start{align-items:flex-start}.sm\:hx-text-xl{font-size:1.25rem}@media not all and (min-width:1024px){.sm\:max-lg\:hx-grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@media(min-width:768px){.md\:hx-sticky{position:sticky}.md\:hx-top-16{top:4rem}.md\:hx-inline-block{display:inline-block}.md\:hx-hidden{display:none}.md\:hx-aspect-\[1\.1\/1\]{aspect-ratio:1.1/1}.md\:hx-h-\[calc\(100vh-var\(--navbar-height\)-var\(--menu-height\)\)\]{height:calc(100vh - var(--navbar-height) - var(--menu-height))}.md\:hx-max-h-\[min\(calc\(100vh-5rem-env\(safe-area-inset-bottom\)\)\2c 400px\)\]{max-height:min(calc(100vh - 5rem - env(safe-area-inset-bottom)),400px)}.md\:hx-w-64{width:16rem}.md\:hx-shrink-0{flex-shrink:0}.md\:hx-grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:hx-justify-start{justify-content:flex-start}.md\:hx-self-start{align-self:flex-start}.md\:hx-px-12{padding-left:3rem;padding-right:3rem}.md\:hx-pt-12{padding-top:3rem}.md\:hx-text-5xl{font-size:3rem}.md\:hx-text-lg{font-size:1.125rem}.md\:hx-text-sm{font-size:.875rem}}@media(min-width:1024px){.lg\:hx-grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1280px){.xl\:hx-block{display:block}.xl\:hx-grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.ltr\:hx-right-1\.5:where([dir=ltr],[dir=ltr] *){right:.375rem}.ltr\:hx-right-3:where([dir=ltr],[dir=ltr] *){right:.75rem}.ltr\:hx--mr-4:where([dir=ltr],[dir=ltr] *){margin-right:-1rem}.ltr\:hx-ml-1:where([dir=ltr],[dir=ltr] *){margin-left:.25rem}.ltr\:hx-ml-3:where([dir=ltr],[dir=ltr] *){margin-left:.75rem}.ltr\:hx-ml-auto:where([dir=ltr],[dir=ltr] *){margin-left:auto}.ltr\:hx-mr-auto:where([dir=ltr],[dir=ltr] *){margin-right:auto}.ltr\:hx-rotate-180:where([dir=ltr],[dir=ltr] *){--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y))rotate(var(--tw-rotate))skewX(var(--tw-skew-x))skewY(var(--tw-skew-y))scaleX(var(--tw-scale-x))scaleY(var(--tw-scale-y))}.ltr\:hx-border-l:where([dir=ltr],[dir=ltr] *){border-left-width:1px}.ltr\:hx-pl-12:where([dir=ltr],[dir=ltr] *){padding-left:3rem}.ltr\:hx-pl-16:where([dir=ltr],[dir=ltr] *){padding-left:4rem}.ltr\:hx-pl-3:where([dir=ltr],[dir=ltr] *){padding-left:.75rem}.ltr\:hx-pl-4:where([dir=ltr],[dir=ltr] *){padding-left:1rem}.ltr\:hx-pl-5:where([dir=ltr],[dir=ltr] *){padding-left:1.25rem}.ltr\:hx-pl-6:where([dir=ltr],[dir=ltr] *){padding-left:1.5rem}.ltr\:hx-pl-8:where([dir=ltr],[dir=ltr] *){padding-left:2rem}.ltr\:hx-pr-0:where([dir=ltr],[dir=ltr] *){padding-right:0}.ltr\:hx-pr-2:where([dir=ltr],[dir=ltr] *){padding-right:.5rem}.ltr\:hx-pr-4:where([dir=ltr],[dir=ltr] *){padding-right:1rem}.ltr\:hx-pr-9:where([dir=ltr],[dir=ltr] *){padding-right:2.25rem}.ltr\:hx-text-right:where([dir=ltr],[dir=ltr] *){text-align:right}.ltr\:before\:hx-left-0:where([dir=ltr],[dir=ltr] *)::before{content:var(--tw-content);left:0}@media(min-width:768px){.ltr\:md\:hx-left-auto:where([dir=ltr],[dir=ltr] *){left:auto}}.rtl\:hx-left-1\.5:where([dir=rtl],[dir=rtl] *){left:.375rem}.rtl\:hx-left-3:where([dir=rtl],[dir=rtl] *){left:.75rem}.rtl\:hx--ml-4:where([dir=rtl],[dir=rtl] *){margin-left:-1rem}.rtl\:hx-ml-auto:where([dir=rtl],[dir=rtl] *){margin-left:auto}.rtl\:hx-mr-1:where([dir=rtl],[dir=rtl] *){margin-right:.25rem}.rtl\:hx-mr-3:where([dir=rtl],[dir=rtl] *){margin-right:.75rem}.rtl\:hx-mr-auto:where([dir=rtl],[dir=rtl] *){margin-right:auto}.rtl\:-hx-rotate-180:where([dir=rtl],[dir=rtl] *){--tw-rotate:-180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y))rotate(var(--tw-rotate))skewX(var(--tw-skew-x))skewY(var(--tw-skew-y))scaleX(var(--tw-scale-x))scaleY(var(--tw-scale-y))}.rtl\:hx-border-r:where([dir=rtl],[dir=rtl] *){border-right-width:1px}.rtl\:hx-pl-2:where([dir=rtl],[dir=rtl] *){padding-left:.5rem}.rtl\:hx-pl-4:where([dir=rtl],[dir=rtl] *){padding-left:1rem}.rtl\:hx-pl-9:where([dir=rtl],[dir=rtl] *){padding-left:2.25rem}.rtl\:hx-pr-12:where([dir=rtl],[dir=rtl] *){padding-right:3rem}.rtl\:hx-pr-16:where([dir=rtl],[dir=rtl] *){padding-right:4rem}.rtl\:hx-pr-3:where([dir=rtl],[dir=rtl] *){padding-right:.75rem}.rtl\:hx-pr-4:where([dir=rtl],[dir=rtl] *){padding-right:1rem}.rtl\:hx-pr-5:where([dir=rtl],[dir=rtl] *){padding-right:1.25rem}.rtl\:hx-pr-6:where([dir=rtl],[dir=rtl] *){padding-right:1.5rem}.rtl\:hx-pr-8:where([dir=rtl],[dir=rtl] *){padding-right:2rem}.rtl\:hx-text-left:where([dir=rtl],[dir=rtl] *){text-align:left}.rtl\:before\:hx-right-0:where([dir=rtl],[dir=rtl] *)::before{content:var(--tw-content);right:0}.rtl\:before\:hx-rotate-180:where([dir=rtl],[dir=rtl] *)::before{content:var(--tw-content);--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y))rotate(var(--tw-rotate))skewX(var(--tw-skew-x))skewY(var(--tw-skew-y))scaleX(var(--tw-scale-x))scaleY(var(--tw-scale-y))}@media(min-width:768px){.rtl\:md\:hx-right-auto:where([dir=rtl],[dir=rtl] *){right:auto}}@media print{.print\:hx-hidden{display:none}.print\:hx-bg-transparent{background-color:transparent}} \ No newline at end of file diff --git a/css/custom.css b/css/custom.css new file mode 100644 index 0000000..e69de29 diff --git a/css/custom.min.e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.css b/css/custom.min.e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.css new file mode 100644 index 0000000..e69de29 diff --git a/docs/01-introduction/index.html b/docs/01-introduction/index.html new file mode 100644 index 0000000..40f1c74 --- /dev/null +++ b/docs/01-introduction/index.html @@ -0,0 +1,328 @@ + + + + + + + + + + + + +Introduction – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      + +
      +
      Documentation
      +
      + +
      +

      Introduction

      +

      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 includes a powerful form feature that provides all these features.

      +

      go-form is heavily influenced by Symfony Form. It includes:

      +
        +
      • A form builder based on fields declarations and independent of structs
      • +
      • Validation based on constraints
      • +
      • Data mounting to populate a form from a struct instance
      • +
      • Data binding to populate a struct instance from a submitted form
      • +
      • Form renderer with customizable themes
      • +
      +

      Questions or Feedback? +

      + +
      fmt.Sprintf("foo")
      + +
      +
      + +
      +
      + + +
      +
      +
      + +
      + + + + + + diff --git a/docs/02-installation/index.html b/docs/02-installation/index.html new file mode 100644 index 0000000..f75c605 --- /dev/null +++ b/docs/02-installation/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + +Installation – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/button/index.html b/docs/button/index.html new file mode 100644 index 0000000..494d61e --- /dev/null +++ b/docs/button/index.html @@ -0,0 +1,467 @@ + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/constraints/index.html b/docs/constraints/index.html new file mode 100644 index 0000000..7acb3cb --- /dev/null +++ b/docs/constraints/index.html @@ -0,0 +1,553 @@ + + + + + + + + + + + + +Constraints – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      + +
      +
      Constraints
      +
      + +
      +

      Constraints

      + +
      +
      + + +
      +
      +
      + +
      + + + + + + diff --git a/docs/constraints/index.xml b/docs/constraints/index.xml new file mode 100644 index 0000000..d872a21 --- /dev/null +++ b/docs/constraints/index.xml @@ -0,0 +1,18 @@ + + + deblan/go-form – Constraints + /docs/constraints/ + Recent content in Constraints on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + diff --git a/docs/field/date/date/index.html b/docs/field/date/date/index.html new file mode 100644 index 0000000..24ea0fe --- /dev/null +++ b/docs/field/date/date/index.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + +Date – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/date/datetime/index.html b/docs/field/date/datetime/index.html new file mode 100644 index 0000000..e81c6e3 --- /dev/null +++ b/docs/field/date/datetime/index.html @@ -0,0 +1,468 @@ + + + + + + + + + + + + +Datetime – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/date/index.html b/docs/field/date/index.html new file mode 100644 index 0000000..26496a4 --- /dev/null +++ b/docs/field/date/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/date/index.xml b/docs/field/date/index.xml new file mode 100644 index 0000000..e8ee8b2 --- /dev/null +++ b/docs/field/date/index.xml @@ -0,0 +1,57 @@ + + + deblan/go-form – + http://localhost:1313/docs/field/date/ + Recent content on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + Date + http://localhost:1313/docs/field/date/date/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/date/date/ + + + + + + + + + Datetime + http://localhost:1313/docs/field/date/datetime/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/date/datetime/ + + + + + + + + + Time + http://localhost:1313/docs/field/date/time/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/date/time/ + + + + + + + + + diff --git a/docs/field/date/time/index.html b/docs/field/date/time/index.html new file mode 100644 index 0000000..a272036 --- /dev/null +++ b/docs/field/date/time/index.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + +Time – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/index.html b/docs/field/index.html new file mode 100644 index 0000000..f760ecf --- /dev/null +++ b/docs/field/index.html @@ -0,0 +1,452 @@ + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/index.xml b/docs/field/index.xml new file mode 100644 index 0000000..97425df --- /dev/null +++ b/docs/field/index.xml @@ -0,0 +1,49 @@ + + + deblan/go-form – + http://localhost:1313/docs/field/ + Recent content on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + Input + http://localhost:1313/docs/field/input/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/input/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + + http://localhost:1313/docs/field/date/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/date/ + + + + + + + + + diff --git a/docs/field/input/hidden/index.html b/docs/field/input/hidden/index.html new file mode 100644 index 0000000..825a09e --- /dev/null +++ b/docs/field/input/hidden/index.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + +Hidden – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/input/index.html b/docs/field/input/index.html new file mode 100644 index 0000000..68a11a4 --- /dev/null +++ b/docs/field/input/index.html @@ -0,0 +1,461 @@ + + + + + + + + + + + + +Input – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/input/index.xml b/docs/field/input/index.xml new file mode 100644 index 0000000..a3d7aea --- /dev/null +++ b/docs/field/input/index.xml @@ -0,0 +1,121 @@ + + + deblan/go-form – Input + http://localhost:1313/docs/field/input/ + Recent content in Input on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + Hidden + http://localhost:1313/docs/field/input/hidden/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/input/hidden/ + + + + + + + + + Mail + http://localhost:1313/docs/field/input/mail/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/input/mail/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Number + http://localhost:1313/docs/field/input/number/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/input/number/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Password + http://localhost:1313/docs/field/input/password/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/input/password/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Range + http://localhost:1313/docs/field/input/range/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/input/range/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Text + http://localhost:1313/docs/field/input/text/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/field/input/text/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + diff --git a/docs/field/input/mail/index.html b/docs/field/input/mail/index.html new file mode 100644 index 0000000..b0aacf8 --- /dev/null +++ b/docs/field/input/mail/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + +Mail – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/input/number/index.html b/docs/field/input/number/index.html new file mode 100644 index 0000000..e35ddda --- /dev/null +++ b/docs/field/input/number/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + +Number – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/input/password/index.html b/docs/field/input/password/index.html new file mode 100644 index 0000000..2484c46 --- /dev/null +++ b/docs/field/input/password/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + +Password – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/input/range/index.html b/docs/field/input/range/index.html new file mode 100644 index 0000000..ba260dd --- /dev/null +++ b/docs/field/input/range/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + + +Range – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/input/text/index.html b/docs/field/input/text/index.html new file mode 100644 index 0000000..6ed4142 --- /dev/null +++ b/docs/field/input/text/index.html @@ -0,0 +1,474 @@ + + + + + + + + + + + + +Text – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/field/text/index.html b/docs/field/text/index.html new file mode 100644 index 0000000..768359e --- /dev/null +++ b/docs/field/text/index.html @@ -0,0 +1,311 @@ + + + + + + + + + + + + +Text – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/button/index.html b/docs/fields/button/index.html new file mode 100644 index 0000000..e9def52 --- /dev/null +++ b/docs/fields/button/index.html @@ -0,0 +1,562 @@ + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/date/date/index.html b/docs/fields/date/date/index.html new file mode 100644 index 0000000..38b5231 --- /dev/null +++ b/docs/fields/date/date/index.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + +Date – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/date/datetime/index.html b/docs/fields/date/datetime/index.html new file mode 100644 index 0000000..67cdac7 --- /dev/null +++ b/docs/fields/date/datetime/index.html @@ -0,0 +1,468 @@ + + + + + + + + + + + + +Datetime – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/date/index.html b/docs/fields/date/index.html new file mode 100644 index 0000000..25b15c9 --- /dev/null +++ b/docs/fields/date/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/date/index.xml b/docs/fields/date/index.xml new file mode 100644 index 0000000..4895401 --- /dev/null +++ b/docs/fields/date/index.xml @@ -0,0 +1,57 @@ + + + deblan/go-form – + http://localhost:1313/docs/fields/date/ + Recent content on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + Date + http://localhost:1313/docs/fields/date/date/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/fields/date/date/ + + + + + + + + + Datetime + http://localhost:1313/docs/fields/date/datetime/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/fields/date/datetime/ + + + + + + + + + Time + http://localhost:1313/docs/fields/date/time/ + Mon, 01 Jan 0001 00:00:00 +0000 + + http://localhost:1313/docs/fields/date/time/ + + + + + + + + + diff --git a/docs/fields/date/time/index.html b/docs/fields/date/time/index.html new file mode 100644 index 0000000..bd0c000 --- /dev/null +++ b/docs/fields/date/time/index.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + +Time – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/index.html b/docs/fields/index.html new file mode 100644 index 0000000..26f7f5c --- /dev/null +++ b/docs/fields/index.html @@ -0,0 +1,553 @@ + + + + + + + + + + + + +Input – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/index.xml b/docs/fields/index.xml new file mode 100644 index 0000000..8f76540 --- /dev/null +++ b/docs/fields/index.xml @@ -0,0 +1,62 @@ + + + deblan/go-form – Input + /docs/fields/ + Recent content in Input on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + Input + /docs/fields/input/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + + /docs/fields/button/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/button/ + + + + + + + + + + /docs/fields/textarea/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/textarea/ + + + + + + + + + diff --git a/docs/fields/input/date/index.html b/docs/fields/input/date/index.html new file mode 100644 index 0000000..198f69a --- /dev/null +++ b/docs/fields/input/date/index.html @@ -0,0 +1,569 @@ + + + + + + + + + + + + +Date – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/datetime/index.html b/docs/fields/input/datetime/index.html new file mode 100644 index 0000000..ce9c8f7 --- /dev/null +++ b/docs/fields/input/datetime/index.html @@ -0,0 +1,569 @@ + + + + + + + + + + + + +Datetime – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/hidden/index.html b/docs/fields/input/hidden/index.html new file mode 100644 index 0000000..6c8c2e4 --- /dev/null +++ b/docs/fields/input/hidden/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + +Hidden – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/index.html b/docs/fields/input/index.html new file mode 100644 index 0000000..e8bef98 --- /dev/null +++ b/docs/fields/input/index.html @@ -0,0 +1,562 @@ + + + + + + + + + + + + +Input – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/index.xml b/docs/fields/input/index.xml new file mode 100644 index 0000000..793efa4 --- /dev/null +++ b/docs/fields/input/index.xml @@ -0,0 +1,160 @@ + + + deblan/go-form – Input + /docs/fields/input/ + Recent content in Input on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + Hidden + /docs/fields/input/hidden/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/hidden/ + + + + + + + + + Date + /docs/fields/input/date/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/date/ + + + + + + + + + Datetime + /docs/fields/input/datetime/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/datetime/ + + + + + + + + + Mail + /docs/fields/input/mail/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/mail/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Number + /docs/fields/input/number/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/number/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Password + /docs/fields/input/password/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/password/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Range + /docs/fields/input/range/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/range/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Text + /docs/fields/input/text/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/text/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Time + /docs/fields/input/time/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/time/ + + + + + + + + + diff --git a/docs/fields/input/mail/index.html b/docs/fields/input/mail/index.html new file mode 100644 index 0000000..948442c --- /dev/null +++ b/docs/fields/input/mail/index.html @@ -0,0 +1,579 @@ + + + + + + + + + + + + +Mail – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/number/index.html b/docs/fields/input/number/index.html new file mode 100644 index 0000000..d3affcc --- /dev/null +++ b/docs/fields/input/number/index.html @@ -0,0 +1,579 @@ + + + + + + + + + + + + +Number – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/password/index.html b/docs/fields/input/password/index.html new file mode 100644 index 0000000..fafd30a --- /dev/null +++ b/docs/fields/input/password/index.html @@ -0,0 +1,579 @@ + + + + + + + + + + + + +Password – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/range/index.html b/docs/fields/input/range/index.html new file mode 100644 index 0000000..a6d0c57 --- /dev/null +++ b/docs/fields/input/range/index.html @@ -0,0 +1,579 @@ + + + + + + + + + + + + +Range – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/text/index.html b/docs/fields/input/text/index.html new file mode 100644 index 0000000..acc0810 --- /dev/null +++ b/docs/fields/input/text/index.html @@ -0,0 +1,579 @@ + + + + + + + + + + + + +Text – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/input/time/index.html b/docs/fields/input/time/index.html new file mode 100644 index 0000000..e7d6797 --- /dev/null +++ b/docs/fields/input/time/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + +Time – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/fields/textarea/index.html b/docs/fields/textarea/index.html new file mode 100644 index 0000000..74ac5ef --- /dev/null +++ b/docs/fields/textarea/index.html @@ -0,0 +1,562 @@ + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/form/bind/index.html b/docs/form/bind/index.html new file mode 100644 index 0000000..1cf1d29 --- /dev/null +++ b/docs/form/bind/index.html @@ -0,0 +1,651 @@ + + + + + + + + + + + + +Bind – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      + +
      + +
      + Form +
      Bind
      +
      + +
      +

      Bind

      +
      + +
      import (
      +	"net/http"
      +
      +    "gitnet.fr/deblan/go-form/form"
      +)
      +
      +type Person struct {
      +    Name string
      +    Age  int
      +}
      +
      +func createForm() *form.Form {
      +    f := form.NewForm()
      +
      +    // do stuff
      +
      +    return f
      +}
      +
      +http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
      +    alice := Person{
      +        Name: "Alice",
      +        Age:  42,
      +    }
      +
      +    myForm := createForm()
      +
      +    if r.Method == myForm.Method {
      +        myForm.HandleRequest(r)
      +
      +        if myForm.IsSubmitted() && myForm.IsValid() {
      +            myForm.Bind(&data)
      +        }
      +    }
      +})
      + +
      +
      + +
      +
      + + +
      +
      +
      + +
      + + + + + + diff --git a/docs/form/index.html b/docs/form/index.html new file mode 100644 index 0000000..94b3924 --- /dev/null +++ b/docs/form/index.html @@ -0,0 +1,669 @@ + + + + + + + + + + + + +Form – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      + +
      +
      Form
      +
      + +
      +

      Form

      +

      A form is a struct containing:

      +
        +
      • Fields
      • +
      • Options
      • +
      • Method
      • +
      • Action
      • +
      +

      Import +

      + +
      import (
      +	"net/http"
      +
      +    "gitnet.fr/deblan/go-form/form"
      +)
      + +
      +
      +

      Usage +

      + +
      // Let's create a new form
      +// You can pass *form.Field as arguments
      +myForm := form.NewForm(field1, field2, ...)
      +
      +// Add somes fields
      +myForm.Add(field3, field4, ...)
      +
      +// Set the method
      +// <form method="POST" ...>
      +myForm.WithMethod(http.MethodPost)
      +
      +// Define the action
      +// <form action="/" ...>
      +myForm.WithAction("/")
      +
      +// Set a name
      +myForm.WithName("myForm")
      +
      +// Add options
      +myForm.WithOptions(option1, option2, ...)
      +
      +// When all fields are added, call End()
      +myForm.End()
      + +
      +
      +

      Attributes +

      Some options are natively supported in go-form themes.

      +
      + +
      myForm.WithOptions(
      +    form.NewOption("help", "A help for the form"),
      +    // <form data-foo="bar" data-bar="bar" ...
      +    form.NewOption("attr", map[string]string{
      +        "data-foo": "foo",
      +        "data-bar": "bar",
      +    }),
      +)
      + +
      +
      + +
      +
      + + +
      +
      +
      + +
      + + + + + + diff --git a/docs/form/index.xml b/docs/form/index.xml new file mode 100644 index 0000000..1ce3e96 --- /dev/null +++ b/docs/form/index.xml @@ -0,0 +1,113 @@ + + + deblan/go-form – Form + /docs/form/ + Recent content in Form on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + Mount + /docs/form/mount/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/form/mount/ + + + + <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> + +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Person</span> <span class="kd">struct</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Age</span> <span class="kt">int</span> +</span></span><span class="line"><span class="cl"><span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="nx">alice</span> <span class="o">:=</span> <span class="nx">Person</span><span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Age</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span> +</span></span><span class="line"><span class="cl"><span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="c1">// Assuming 2 fields named &#34;Name&#34; and &#34;Age&#34; exist</span> +</span></span><span class="line"><span class="cl"><span class="nx">myForm</span><span class="p">.</span><span class="nf">Mount</span><span class="p">(</span><span class="nx">alice</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> + <button + class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" + title="Copy code" + > + <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> + <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> + </button> +</div> +</div> + + + + + + Bind + /docs/form/bind/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/form/bind/ + + + + <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> + +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> +</span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="s">&#34;gitnet.fr/deblan/go-form/form&#34;</span> +</span></span><span class="line"><span class="cl"><span class="p">)</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Person</span> <span class="kd">struct</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Age</span> <span class="kt">int</span> +</span></span><span class="line"><span class="cl"><span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">createForm</span><span class="p">()</span> <span class="o">*</span><span class="nx">form</span><span class="p">.</span><span class="nx">Form</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">f</span> <span class="o">:=</span> <span class="nx">form</span><span class="p">.</span><span class="nf">NewForm</span><span class="p">()</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="c1">// do stuff</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">f</span> +</span></span><span class="line"><span class="cl"><span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">alice</span> <span class="o">:=</span> <span class="nx">Person</span><span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Age</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span> +</span></span><span class="line"><span class="cl"> <span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span> <span class="o">:=</span> <span class="nf">createForm</span><span class="p">()</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Method</span> <span class="o">==</span> <span class="nx">myForm</span><span class="p">.</span><span class="nx">Method</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span><span class="p">.</span><span class="nf">HandleRequest</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">myForm</span><span class="p">.</span><span class="nf">IsSubmitted</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="nx">myForm</span><span class="p">.</span><span class="nf">IsValid</span><span class="p">()</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span><span class="p">.</span><span class="nf">Bind</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">data</span><span class="p">)</span> +</span></span><span class="line"><span class="cl"> <span class="p">}</span> +</span></span><span class="line"><span class="cl"> <span class="p">}</span> +</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> + <button + class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" + title="Copy code" + > + <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> + <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> + </button> +</div> +</div> + + + + + + diff --git a/docs/form/mount/index.html b/docs/form/mount/index.html new file mode 100644 index 0000000..2024828 --- /dev/null +++ b/docs/form/mount/index.html @@ -0,0 +1,605 @@ + + + + + + + + + + + + +Mount – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      + +
      + +
      + Form +
      Mount
      +
      + +
      +

      Mount

      +
      + +
      type Person struct {
      +    Name string
      +    Age  int
      +}
      +
      +alice := Person{
      +    Name: "Alice",
      +    Age:  42,
      +}
      +
      +// Assuming 2 fields named "Name" and "Age" exist
      +myForm.Mount(alice)
      + +
      +
      + +
      +
      + + +
      +
      +
      + +
      + + + + + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..7be1e4a --- /dev/null +++ b/docs/index.html @@ -0,0 +1,609 @@ + + + + + + + + + + + + +Introduction – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      + +
      Documentation
      +
      + +
      +

      Introduction

      +

      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 includes a powerful form feature that provides all these features.

      +

      go-form is heavily influenced by Symfony Form. It includes:

      +
        +
      • A form builder based on fields declarations and independent of structs
      • +
      • Validation based on constraints
      • +
      • Data mounting to populate a form from a struct instance
      • +
      • Data binding to populate a struct instance from a submitted form
      • +
      • Form renderer with customizable themes
      • +
      +

      Questions or Feedback? +

      + +
      fmt.Sprintf("foo")
      + +
      +
      + +
      +
      + + +
      +
      +
      + +
      + + + + + + diff --git a/docs/index.xml b/docs/index.xml new file mode 100644 index 0000000..967ef56 --- /dev/null +++ b/docs/index.xml @@ -0,0 +1,307 @@ + + + deblan/go-form – Introduction + /docs/ + Recent content in Introduction on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + Mount + /docs/form/mount/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/form/mount/ + + + + <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> + +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Person</span> <span class="kd">struct</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Age</span> <span class="kt">int</span> +</span></span><span class="line"><span class="cl"><span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="nx">alice</span> <span class="o">:=</span> <span class="nx">Person</span><span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Age</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span> +</span></span><span class="line"><span class="cl"><span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="c1">// Assuming 2 fields named &#34;Name&#34; and &#34;Age&#34; exist</span> +</span></span><span class="line"><span class="cl"><span class="nx">myForm</span><span class="p">.</span><span class="nf">Mount</span><span class="p">(</span><span class="nx">alice</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> + <button + class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" + title="Copy code" + > + <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> + <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> + </button> +</div> +</div> + + + + + + Bind + /docs/form/bind/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/form/bind/ + + + + <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> + +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> +</span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="s">&#34;gitnet.fr/deblan/go-form/form&#34;</span> +</span></span><span class="line"><span class="cl"><span class="p">)</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Person</span> <span class="kd">struct</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Age</span> <span class="kt">int</span> +</span></span><span class="line"><span class="cl"><span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">createForm</span><span class="p">()</span> <span class="o">*</span><span class="nx">form</span><span class="p">.</span><span class="nx">Form</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">f</span> <span class="o">:=</span> <span class="nx">form</span><span class="p">.</span><span class="nf">NewForm</span><span class="p">()</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="c1">// do stuff</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">f</span> +</span></span><span class="line"><span class="cl"><span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">alice</span> <span class="o">:=</span> <span class="nx">Person</span><span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> +</span></span><span class="line"><span class="cl"> <span class="nx">Age</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span> +</span></span><span class="line"><span class="cl"> <span class="p">}</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span> <span class="o">:=</span> <span class="nf">createForm</span><span class="p">()</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Method</span> <span class="o">==</span> <span class="nx">myForm</span><span class="p">.</span><span class="nx">Method</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span><span class="p">.</span><span class="nf">HandleRequest</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span> +</span></span><span class="line"><span class="cl"> +</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">myForm</span><span class="p">.</span><span class="nf">IsSubmitted</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="nx">myForm</span><span class="p">.</span><span class="nf">IsValid</span><span class="p">()</span> <span class="p">{</span> +</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span><span class="p">.</span><span class="nf">Bind</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">data</span><span class="p">)</span> +</span></span><span class="line"><span class="cl"> <span class="p">}</span> +</span></span><span class="line"><span class="cl"> <span class="p">}</span> +</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> + <button + class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" + title="Copy code" + > + <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> + <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> + </button> +</div> +</div> + + + + + + Hidden + /docs/fields/input/hidden/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/hidden/ + + + + + + + + + Installation + /docs/installation/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/installation/ + + + + <h1>Installation</h1> + + + + + + /docs/fields/button/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/button/ + + + + + + + + + + /docs/fields/textarea/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/textarea/ + + + + + + + + + + /docs/rendering/theming/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/rendering/theming/ + + + + + + + + + Date + /docs/fields/input/date/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/date/ + + + + + + + + + Datetime + /docs/fields/input/datetime/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/datetime/ + + + + + + + + + Mail + /docs/fields/input/mail/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/mail/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Number + /docs/fields/input/number/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/number/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Password + /docs/fields/input/password/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/password/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Range + /docs/fields/input/range/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/range/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Text + /docs/fields/input/text/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/text/ + + + + <table> + <tr> + <td>ok</td> + </tr> +</table> + + + + + + Time + /docs/fields/input/time/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/fields/input/time/ + + + + + + + + + diff --git a/docs/installation/index.html b/docs/installation/index.html new file mode 100644 index 0000000..9f6506f --- /dev/null +++ b/docs/installation/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + +Installation – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      + +
      +
      Installation
      +
      + +
      +

      Installation

      +

      Installation

      +
      +
      + + +
      +
      +
      + +
      + + + + + + diff --git a/docs/introduction/index.html b/docs/introduction/index.html new file mode 100644 index 0000000..f9d25fa --- /dev/null +++ b/docs/introduction/index.html @@ -0,0 +1,368 @@ + + + + + + + + + + + + +Introduction – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      + +
      +
      Introduction
      +
      + +
      +

      Introduction

      +

      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 includes a powerful form feature that provides all these features.

      +

      go-form is heavily influenced by Symfony Form. It includes:

      +
        +
      • A form builder based on fields declarations and independent of structs
      • +
      • Validation based on constraints
      • +
      • Data mounting to populate a form from a struct instance
      • +
      • Data binding to populate a struct instance from a submitted form
      • +
      • Form renderer with customizable themes
      • +
      +

      Questions or Feedback? +

      + +
      fmt.Sprintf("foo")
      + +
      +
      + +
      +
      + + +
      +
      +
      + +
      + + + + + + diff --git a/docs/rendering/index.html b/docs/rendering/index.html new file mode 100644 index 0000000..220abb1 --- /dev/null +++ b/docs/rendering/index.html @@ -0,0 +1,553 @@ + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/docs/rendering/index.xml b/docs/rendering/index.xml new file mode 100644 index 0000000..761ec73 --- /dev/null +++ b/docs/rendering/index.xml @@ -0,0 +1,31 @@ + + + deblan/go-form – + /docs/rendering/ + Recent content on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + + /docs/rendering/theming/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /docs/rendering/theming/ + + + + + + + + + diff --git a/docs/rendering/theming/index.html b/docs/rendering/theming/index.html new file mode 100644 index 0000000..8b7ff19 --- /dev/null +++ b/docs/rendering/theming/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + + +
      + +
      + + + + + + diff --git a/en.search-data.json b/en.search-data.json new file mode 100644 index 0000000..6a989d0 --- /dev/null +++ b/en.search-data.json @@ -0,0 +1 @@ +{"/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes ","questions-or-feedback#Questions or Feedback?":" fmt.Sprintf(\"foo\") "},"title":"Documentation"},"/docs/fields/input/":{"data":{"":" ok "},"title":"Input"},"/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/docs/fields/input/text/":{"data":{"":" ok "},"title":"Text"},"/docs/form/":{"data":{"":"A form is a struct containing:\nFields Options Method Action ","attributes#Attributes":"Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) ","import#Import":" import ( \"net/http\" \"gitnet.fr/deblan/go-form/form\" ) ","usage#Usage":" // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() "},"title":"Form"},"/docs/form/bind/":{"data":{"":" import ( \"net/http\" \"gitnet.fr/deblan/go-form/form\" ) type Person struct { Name string Age int } func createForm() *form.Form { f := form.NewForm() // do stuff return f } http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { alice := Person{ Name: \"Alice\", Age: 42, } myForm := createForm() if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Bind"},"/docs/form/mount/":{"data":{"":" type Person struct { Name string Age int } alice := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(alice) "},"title":"Mount"},"/docs/installation/":{"data":{"installation#Installation":"Installation"},"title":"Installation"}} \ No newline at end of file diff --git a/en.search.js b/en.search.js new file mode 100644 index 0000000..6e1bdfb --- /dev/null +++ b/en.search.js @@ -0,0 +1,437 @@ +// Search functionality using FlexSearch. + +// Change shortcut key to cmd+k on Mac, iPad or iPhone. +document.addEventListener("DOMContentLoaded", function () { + if (/iPad|iPhone|Macintosh/.test(navigator.userAgent)) { + // select the kbd element under the .search-wrapper class + const keys = document.querySelectorAll(".search-wrapper kbd"); + keys.forEach(key => { + key.innerHTML = 'K'; + }); + } +}); + +// Render the search data as JSON. +// +// +// +// + +(function () { + const searchDataURL = '/en.search-data.json'; + + const inputElements = document.querySelectorAll('.search-input'); + for (const el of inputElements) { + el.addEventListener('focus', init); + el.addEventListener('keyup', search); + el.addEventListener('keydown', handleKeyDown); + el.addEventListener('input', handleInputChange); + } + + const shortcutElements = document.querySelectorAll('.search-wrapper kbd'); + + function setShortcutElementsOpacity(opacity) { + shortcutElements.forEach(el => { + el.style.opacity = opacity; + }); + } + + function handleInputChange(e) { + const opacity = e.target.value.length > 0 ? 0 : 100; + setShortcutElementsOpacity(opacity); + } + + // Get the search wrapper, input, and results elements. + function getActiveSearchElement() { + const inputs = Array.from(document.querySelectorAll('.search-wrapper')).filter(el => el.clientHeight > 0); + if (inputs.length === 1) { + return { + wrapper: inputs[0], + inputElement: inputs[0].querySelector('.search-input'), + resultsElement: inputs[0].querySelector('.search-results') + }; + } + return undefined; + } + + const INPUTS = ['input', 'select', 'button', 'textarea'] + + // Focus the search input when pressing ctrl+k/cmd+k or /. + document.addEventListener('keydown', function (e) { + const { inputElement } = getActiveSearchElement(); + if (!inputElement) return; + + const activeElement = document.activeElement; + const tagName = activeElement && activeElement.tagName; + if ( + inputElement === activeElement || + !tagName || + INPUTS.includes(tagName) || + (activeElement && activeElement.isContentEditable)) + return; + + if ( + e.key === '/' || + (e.key === 'k' && + (e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey)) + ) { + e.preventDefault(); + inputElement.focus(); + } else if (e.key === 'Escape' && inputElement.value) { + inputElement.blur(); + } + }); + + // Dismiss the search results when clicking outside the search box. + document.addEventListener('mousedown', function (e) { + const { inputElement, resultsElement } = getActiveSearchElement(); + if (!inputElement || !resultsElement) return; + if ( + e.target !== inputElement && + e.target !== resultsElement && + !resultsElement.contains(e.target) + ) { + setShortcutElementsOpacity(100); + hideSearchResults(); + } + }); + + // Get the currently active result and its index. + function getActiveResult() { + const { resultsElement } = getActiveSearchElement(); + if (!resultsElement) return { result: undefined, index: -1 }; + + const result = resultsElement.querySelector('.active'); + if (!result) return { result: undefined, index: -1 }; + + const index = parseInt(result.dataset.index, 10); + return { result, index }; + } + + // Set the active result by index. + function setActiveResult(index) { + const { resultsElement } = getActiveSearchElement(); + if (!resultsElement) return; + + const { result: activeResult } = getActiveResult(); + activeResult && activeResult.classList.remove('active'); + const result = resultsElement.querySelector(`[data-index="${index}"]`); + if (result) { + result.classList.add('active'); + result.focus(); + } + } + + // Get the number of search results from the DOM. + function getResultsLength() { + const { resultsElement } = getActiveSearchElement(); + if (!resultsElement) return 0; + return resultsElement.dataset.count; + } + + // Finish the search by hiding the results and clearing the input. + function finishSearch() { + const { inputElement } = getActiveSearchElement(); + if (!inputElement) return; + hideSearchResults(); + inputElement.value = ''; + inputElement.blur(); + } + + function hideSearchResults() { + const { resultsElement } = getActiveSearchElement(); + if (!resultsElement) return; + resultsElement.classList.add('hx-hidden'); + } + + // Handle keyboard events. + function handleKeyDown(e) { + const { inputElement } = getActiveSearchElement(); + if (!inputElement) return; + + const resultsLength = getResultsLength(); + const { result: activeResult, index: activeIndex } = getActiveResult(); + + switch (e.key) { + case 'ArrowUp': + e.preventDefault(); + if (activeIndex > 0) setActiveResult(activeIndex - 1); + break; + case 'ArrowDown': + e.preventDefault(); + if (activeIndex + 1 < resultsLength) setActiveResult(activeIndex + 1); + break; + case 'Enter': + e.preventDefault(); + if (activeResult) { + activeResult.click(); + } + finishSearch(); + case 'Escape': + e.preventDefault(); + hideSearchResults(); + // Clear the input when pressing escape + inputElement.value = ''; + inputElement.dispatchEvent(new Event('input')); + // Remove focus from the input + inputElement.blur(); + break; + } + } + + // Initializes the search. + function init(e) { + e.target.removeEventListener('focus', init); + if (!(window.pageIndex && window.sectionIndex)) { + preloadIndex(); + } + } + + /** + * Preloads the search index by fetching data and adding it to the FlexSearch index. + * @returns {Promise} A promise that resolves when the index is preloaded. + */ + async function preloadIndex() { + const tokenize = 'forward'; + + const isCJK = () => { + const lang = document.documentElement.lang || "en"; + return lang.startsWith("zh") || lang.startsWith("ja") || lang.startsWith("ko"); + } + + const encodeCJK = (str) => str.replace(/[\x00-\x7F]/g, "").split(""); + const encodeDefault = (str) => (""+str).toLocaleLowerCase().split(/[\p{Z}\p{S}\p{P}\p{C}]+/u); + const encodeFunction = isCJK() ? encodeCJK : encodeDefault; + + window.pageIndex = new FlexSearch.Document({ + tokenize, + encode: encodeFunction, + cache: 100, + document: { + id: 'id', + store: ['title', 'crumb'], + index: "content" + } + }); + + window.sectionIndex = new FlexSearch.Document({ + tokenize, + encode: encodeFunction, + cache: 100, + document: { + id: 'id', + store: ['title', 'content', 'url', 'display', 'crumb'], + index: "content", + tag: 'pageId' + } + }); + + const resp = await fetch(searchDataURL); + const data = await resp.json(); + let pageId = 0; + for (const route in data) { + let pageContent = ''; + ++pageId; + const urlParts = route.split('/').filter(x => x != "" && !x.startsWith('#')); + + let crumb = ''; + let searchUrl = '/' + for (let i = 0; i < urlParts.length; i++) { + const urlPart = urlParts[i]; + searchUrl += urlPart + '/' + + const crumbData = data[searchUrl]; + if (!crumbData) { + console.warn('Excluded page', searchUrl, '- will not be included for search result breadcrumb for', route); + continue; + } + + let title = data[searchUrl].title; + if (title == "_index") { + title = urlPart.split("-").map(x => x).join(" "); + } + crumb += title; + + if (i < urlParts.length - 1) { + crumb += ' > '; + } + } + + for (const heading in data[route].data) { + const [hash, text] = heading.split('#'); + const url = route.trimEnd('/') + (hash ? '#' + hash : ''); + const title = text || data[route].title; + + const content = data[route].data[heading] || ''; + const paragraphs = content.split('\n').filter(Boolean); + + sectionIndex.add({ + id: url, + url, + title, + crumb, + pageId: `page_${pageId}`, + content: title, + ...(paragraphs[0] && { display: paragraphs[0] }) + }); + + for (let i = 0; i < paragraphs.length; i++) { + sectionIndex.add({ + id: `${url}_${i}`, + url, + title, + crumb, + pageId: `page_${pageId}`, + content: paragraphs[i] + }); + } + + pageContent += ` ${title} ${content}`; + } + + window.pageIndex.add({ + id: pageId, + title: data[route].title, + crumb, + content: pageContent + }); + + } + } + + /** + * Performs a search based on the provided query and displays the results. + * @param {Event} e - The event object. + */ + function search(e) { + const query = e.target.value; + if (!e.target.value) { + hideSearchResults(); + return; + } + + const { resultsElement } = getActiveSearchElement(); + while (resultsElement.firstChild) { + resultsElement.removeChild(resultsElement.firstChild); + } + resultsElement.classList.remove('hx-hidden'); + + const pageResults = window.pageIndex.search(query, 5, { enrich: true, suggest: true })[0]?.result || []; + + const results = []; + const pageTitleMatches = {}; + + for (let i = 0; i < pageResults.length; i++) { + const result = pageResults[i]; + pageTitleMatches[i] = 0; + + // Show the top 5 results for each page + const sectionResults = window.sectionIndex.search(query, 5, { enrich: true, suggest: true, tag: `page_${result.id}` })[0]?.result || []; + let isFirstItemOfPage = true + const occurred = {} + + for (let j = 0; j < sectionResults.length; j++) { + const { doc } = sectionResults[j] + const isMatchingTitle = doc.display !== undefined + if (isMatchingTitle) { + pageTitleMatches[i]++ + } + const { url, title } = doc + const content = doc.display || doc.content + + if (occurred[url + '@' + content]) continue + occurred[url + '@' + content] = true + results.push({ + _page_rk: i, + _section_rk: j, + route: url, + prefix: isFirstItemOfPage ? result.doc.crumb : undefined, + children: { title, content } + }) + isFirstItemOfPage = false + } + } + const sortedResults = results + .sort((a, b) => { + // Sort by number of matches in the title. + if (a._page_rk === b._page_rk) { + return a._section_rk - b._section_rk + } + if (pageTitleMatches[a._page_rk] !== pageTitleMatches[b._page_rk]) { + return pageTitleMatches[b._page_rk] - pageTitleMatches[a._page_rk] + } + return a._page_rk - b._page_rk + }) + .map(res => ({ + id: `${res._page_rk}_${res._section_rk}`, + route: res.route, + prefix: res.prefix, + children: res.children + })); + displayResults(sortedResults, query); + } + + /** + * Displays the search results on the page. + * + * @param {Array} results - The array of search results. + * @param {string} query - The search query. + */ + function displayResults(results, query) { + const { resultsElement } = getActiveSearchElement(); + if (!resultsElement) return; + + if (!results.length) { + resultsElement.innerHTML = `No results found.`; + return; + } + + // Highlight the query in the result text. + function highlightMatches(text, query) { + const escapedQuery = query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'); + const regex = new RegExp(escapedQuery, 'gi'); + return text.replace(regex, (match) => `${match}`); + } + + // Create a DOM element from the HTML string. + function createElement(str) { + const div = document.createElement('div'); + div.innerHTML = str.trim(); + return div.firstChild; + } + + function handleMouseMove(e) { + const target = e.target.closest('a'); + if (target) { + const active = resultsElement.querySelector('a.active'); + if (active) { + active.classList.remove('active'); + } + target.classList.add('active'); + } + } + + const fragment = document.createDocumentFragment(); + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.prefix) { + fragment.appendChild(createElement(` +
      ${result.prefix}
      `)); + } + let li = createElement(` +
    • + +
      `+ highlightMatches(result.children.title, query) + `
      ` + + (result.children.content ? + `
      ` + highlightMatches(result.children.content, query) + `
      ` : '') + ` +
      +
    • `); + li.addEventListener('mousemove', handleMouseMove); + li.addEventListener('keydown', handleKeyDown); + li.querySelector('a').addEventListener('click', finishSearch); + fragment.appendChild(li); + } + resultsElement.appendChild(fragment); + resultsElement.dataset.count = results.length; + } +})(); diff --git a/en.search.min.7918d81999396a56d9ff1b7a986d6474e66bd5acdbbe01e8bac129f5ef345a88.js b/en.search.min.7918d81999396a56d9ff1b7a986d6474e66bd5acdbbe01e8bac129f5ef345a88.js new file mode 100644 index 0000000..db3c3e3 --- /dev/null +++ b/en.search.min.7918d81999396a56d9ff1b7a986d6474e66bd5acdbbe01e8bac129f5ef345a88.js @@ -0,0 +1,8 @@ +document.addEventListener("DOMContentLoaded",function(){if(/iPad|iPhone|Macintosh/.test(navigator.userAgent)){const e=document.querySelectorAll(".search-wrapper kbd");e.forEach(e=>{e.innerHTML='K'})}}),function(){const h="/en.search-data.json",u=document.querySelectorAll(".search-input");for(const e of u)e.addEventListener("focus",o),e.addEventListener("keyup",p),e.addEventListener("keydown",n),e.addEventListener("input",m);const d=document.querySelectorAll(".search-wrapper kbd");function a(e){d.forEach(t=>{t.style.opacity=e})}function m(e){const t=e.target.value.length>0?0:100;a(t)}function e(){const e=Array.from(document.querySelectorAll(".search-wrapper")).filter(e=>e.clientHeight>0);return e.length===1?{wrapper:e[0],inputElement:e[0].querySelector(".search-input"),resultsElement:e[0].querySelector(".search-results")}:0[0]}const l=["input","select","button","textarea"];document.addEventListener("keydown",function(t){const{inputElement:n}=e();if(!n)return;const s=document.activeElement,o=s&&s.tagName;if(n===s||!o||l.includes(o)||s&&s.isContentEditable)return;t.key==="/"||t.key==="k"&&(t.metaKey||t.ctrlKey)?(t.preventDefault(),n.focus()):t.key==="Escape"&&n.value&&n.blur()}),document.addEventListener("mousedown",function(n){const{inputElement:o,resultsElement:s}=e();if(!o||!s)return;n.target!==o&&n.target!==s&&!s.contains(n.target)&&(a(100),t())});function r(){const{resultsElement:n}=e();if(!n)return{result:0[0],index:-1};const t=n.querySelector(".active");if(!t)return{result:0[0],index:-1};const s=parseInt(t.dataset.index,10);return{result:t,index:s}}function s(t){const{resultsElement:s}=e();if(!s)return;const{result:o}=r();o&&o.classList.remove("active");const n=s.querySelector(`[data-index="${t}"]`);n&&(n.classList.add("active"),n.focus())}function c(){const{resultsElement:t}=e();return t?t.dataset.count:0}function i(){const{inputElement:n}=e();if(!n)return;t(),n.value="",n.blur()}function t(){const{resultsElement:t}=e();if(!t)return;t.classList.add("hx-hidden")}function n(n){const{inputElement:o}=e();if(!o)return;const d=c(),{result:l,index:a}=r();switch(n.key){case"ArrowUp":n.preventDefault(),a>0&&s(a-1);break;case"ArrowDown":n.preventDefault(),a+1{const e=document.documentElement.lang||"en";return e.startsWith("zh")||e.startsWith("ja")||e.startsWith("ko")},i=e=>e.replace(/[\x00-\x7F]/g,"").split(""),a=e=>(""+e).toLocaleLowerCase().split(/[\p{Z}\p{S}\p{P}\p{C}]+/u),s=o()?i:a;window.pageIndex=new FlexSearch.Document({tokenize:n,encode:s,cache:100,document:{id:"id",store:["title","crumb"],index:"content"}}),window.sectionIndex=new FlexSearch.Document({tokenize:n,encode:s,cache:100,document:{id:"id",store:["title","content","url","display","crumb"],index:"content",tag:"pageId"}});const r=await fetch(h),e=await r.json();let t=0;for(const n in e){let a="";++t;const i=n.split("/").filter(e=>e!=""&&!e.startsWith("#"));let s="",o="/";for(let t=0;te).join(" ")),s+=a,t ")}for(const c in e[n].data){const[l,u]=c.split("#"),o=n.trimEnd("/")+(l?"#"+l:""),i=u||e[n].title,d=e[n].data[c]||"",r=d.split(` +`).filter(Boolean);sectionIndex.add({id:o,url:o,title:i,crumb:s,pageId:`page_${t}`,content:i,...r[0]&&{display:r[0]}});for(let e=0;ee._page_rk===t._page_rk?e._section_rk-t._section_rk:s[e._page_rk]!==s[t._page_rk]?s[t._page_rk]-s[e._page_rk]:e._page_rk-t._page_rk).map(e=>({id:`${e._page_rk}_${e._section_rk}`,route:e.route,prefix:e.prefix,children:e.children}));g(c,i)}function g(t,s){const{resultsElement:o}=e();if(!o)return;if(!t.length){o.innerHTML=`No results found.`;return}function r(e,t){const n=t.replace(/[-\\^$*+?.()|[\]{}]/g,"\\$&"),s=new RegExp(n,"gi");return e.replace(s,e=>`${e}`)}function c(e){const t=document.createElement("div");return t.innerHTML=e.trim(),t.firstChild}function l(e){const t=e.target.closest("a");if(t){const e=o.querySelector("a.active");e&&e.classList.remove("active"),t.classList.add("active")}}const a=document.createDocumentFragment();for(let o=0;o${e.prefix}`));let d=c(` +
    • + +
      `+r(e.children.title,s)+`
      `+(e.children.content?`
      `+r(e.children.content,s)+`
      `:"")+` +
      +
    • `);d.addEventListener("mousemove",l),d.addEventListener("keydown",n),d.querySelector("a").addEventListener("click",i),a.appendChild(d)}o.appendChild(a),o.dataset.count=t.length}}() \ No newline at end of file diff --git a/favicon-16x16.png b/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2dd2b2eb092162cc3cb22473ae565ddf2db9c7 GIT binary patch literal 340 zcmV-a0jvIrP)Px$4oO5oR5(wi(?2VQVHC&l&%^2k$bi`8-++`7#UP56mmo8oO4ltUkH`0000 literal 0 HcmV?d00001 diff --git a/favicon-32x32.png b/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1aea58495d1cfb77aac38b4f5ce0ae14ce6a6f GIT binary patch literal 753 zcmVPx%t4TybR9Hvt*Gs5Xbri?(&+Q?o^Z=zm1S7&Q(q7P@Ng)|dgc2QPnnNi=DNKz5 z5qeOdq~ zid!3;U&PyZAAdA~Gy&XzWq1eo;urkg;50UYJcn0s6#openG9e7cH*gGycZjBr2*hZ zyj-B8`YG1qhkka00G`B0xUCrFPVd8|rj(Z7Z^8*YTt5Gbr}0IveLsNNIE^_q<+b>* zW$;SvaeR(sBys`w$=p2O840b;fdZxs8lktQ}S{Q#o0uspedoF5^0 zOjF3|kayu5ENTl7>g^*2ugsstfi3`Zk^9T@`7b0dA*W9vx5~iXC1n$y%m^gY@EtC< z1xO9-DgA$hxZ|c9u%pWr1RyQQ9Rm>F6Blx`|L@NK2o`CV4(LQM-$}l>E zY(JH3!n+*-p26W-x6MRN7i=6xiLRRUxs3&i)xXdikds{>Cj zr%w^x-jIRpjc;N%ervk2rVRAjXG_X}T8*q-bxEJkzzJlwpH|WppmLfqy{69}lN4U3 j-C + + diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..553fa15d0e22126b2d923b442890cae56d724b40 GIT binary patch literal 15406 zcmeI1Tc{OP7=XvD(?v^9S(>RG3G7s)#{x+Z3N0Tjvd{;^m!m=iiAhk|Lq%`B8D(Zg zMi?SikW||3LAq!~H-j_~?9{Oov@9{vEbIGlFBw;J_RQ?d(PIDbF*9ra_5W-C>-MkN zNirZAm<%17aP3I$Iz34yB}vlJaqPLXJxNwl*40&cKQ2i&U6>@D=um|k$I7|%w+zPN z>iJsk?W3&t*(dhR7GOsFO9oK#K3Lzr&JxFlV`LRP z5b-R}_P=ASnmA6nAw7P*x6H9!R0gA1%ls*7zae^^Sm}Tp!1bvm|EmbFt0YTdV+HRm zb1pSgMq>PVa6X4UAY3hb0fs}P(_SBpKH_~IjEzq z`;~RetSzV?mvP*^4dHjXhF<~~2luUD$F*MDd9nnBP{->w@b{Dz_nUVhZObv7C+Rx2zpp(p z&yMdW!eIz)c-O)lai0$Qj-7hfl;tUVU*`Ds_16{sLtMvOP^UOw_JY4j+j$cFJBJ!2 zi!oHj{{i%ijMbv;YeUi8u;}mq1>JLt8ZV&dT-XXl{Tr)?3fL{GG8e@%@mJSZ z&JB5XjN4jB+j4Es6X#v}j9~rlyh<7Gucs{J0@`p-D>|p$E8R0g+ij2IsHhmP&mq*` z&%Jwk88K&Y{SP)=$ImQaz`1^Zs-8B?WonU-f0;|!4(Ft6Q#Fj6`=%NMvIcuY3I0jdOIcV(v2xaExBca(c0S8NAV+HTKpe3yc{HG$|cM=C~ zH?+Q!5QLVrBG7*k@QmT~Gv5*K&x2E4+G*e-*bF;iPlaBtn_wXn&Gk_27cn0;w$s6O z`+rBALxgAFeF!ta@7?vDK`>F5n0HQtRbYM<+L?DX?=@d?Y_-A3kP$e20?hxb#k{ZI zVx3)J{yiG{L+o$DW-+lwz2v5S} zC9&_;!$HuNHeZK1;M{1W*uRc(&o?1vUY)OF;4E-1l)*a2`$b6GbRMk%?ba)vu_l3M zuCx#Djn`DDS4Lr=2V(nMDbshY=RC*ZMA!ncZO`w^p;p^eJwfi|Dg8Yp_|x}<6EurI zcs6|+(%%|_-ES!GEN&`S(>38u2=>F(v*$9X70ES8o6%U*LOhH9`Xa_ zW&bCkN-__!V*dr@9nffT5BI;I5c_a-O`czavlSV({5^BQcr`<=#%R38`8%Ge!Pz?M zgcx`|3U9zbXqFh4N5FBP_Weo?4sC1N#`io=uE9>(Dz5tS@38e+e{m+++R(aUR?Z=Gp#+dfM;$+YbFL z#%n#)+x~6XsAd1F_xIWds~gaMzbU&nYOg=#8l3HgdgE{XuV5_nmze**2KsJ9`wwBq zIeZ2*Tg+)^LwX;sXa0s5m~&p$9&rOG*cE}{2-)# zo6D3HbEdQn?;S@|pjmTyIJW)n75iO5Sy?e>b?=I8y+_%Ivc?MNo0~iZu|NBLUV-|t zif0D*uGroI$~^Biava&8H^FX*{q<7jS*onKXDx#`m(~6z7zK?I&n~MV?r&r7oErqS zh-YWd&~Z!-Q}!SLP(jTon% zDb_%IzJ8gq3!v71uSc?Mteb68m>K?1Ipacnnv2wvOwi z-*KC + + diff --git a/images/logo-dark.svg b/images/logo-dark.svg new file mode 100644 index 0000000..2857264 --- /dev/null +++ b/images/logo-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/logo.svg b/images/logo.svg new file mode 100644 index 0000000..1ed7daf --- /dev/null +++ b/images/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/index.html b/index.html index 1333ed7..8886d90 100644 --- a/index.html +++ b/index.html @@ -1 +1,449 @@ -TODO + + + + + + + + + + + + + +deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + +
      + +
      +

      + + + + + + diff --git a/index.xml b/index.xml new file mode 100644 index 0000000..27ad978 --- /dev/null +++ b/index.xml @@ -0,0 +1,17 @@ + + + deblan/go-form – Welcome! + http://localhost:1313/ + Recent content in Welcome! on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..7d34dbb --- /dev/null +++ b/js/main.js @@ -0,0 +1,320 @@ +// Light / Dark theme toggle +(function () { + const defaultTheme = 'system' + + const themeToggleButtons = document.querySelectorAll(".theme-toggle"); + + // Change the icons of the buttons based on previous settings or system theme + if ( + localStorage.getItem("color-theme") === "dark" || + (!("color-theme" in localStorage) && + ((window.matchMedia("(prefers-color-scheme: dark)").matches && defaultTheme === "system") || defaultTheme === "dark")) + ) { + themeToggleButtons.forEach((el) => el.dataset.theme = "dark"); + } else { + themeToggleButtons.forEach((el) => el.dataset.theme = "light"); + } + + // Add click event handler to the buttons + themeToggleButtons.forEach((el) => { + el.addEventListener("click", function () { + if (localStorage.getItem("color-theme")) { + if (localStorage.getItem("color-theme") === "light") { + setDarkTheme(); + localStorage.setItem("color-theme", "dark"); + } else { + setLightTheme(); + localStorage.setItem("color-theme", "light"); + } + } else { + if (document.documentElement.classList.contains("dark")) { + setLightTheme(); + localStorage.setItem("color-theme", "light"); + } else { + setDarkTheme(); + localStorage.setItem("color-theme", "dark"); + } + } + el.dataset.theme = document.documentElement.classList.contains("dark") ? "dark" : "light"; + }); + }); + + // Listen for system theme changes + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => { + if (defaultTheme === "system" && !("color-theme" in localStorage)) { + e.matches ? setDarkTheme() : setLightTheme(); + themeToggleButtons.forEach((el) => + el.dataset.theme = document.documentElement.classList.contains("dark") ? "dark" : "light" + ); + } + }); +})(); + +; +// Hamburger menu for mobile navigation + +document.addEventListener('DOMContentLoaded', function () { + const menu = document.querySelector('.hamburger-menu'); + const overlay = document.querySelector('.mobile-menu-overlay'); + const sidebarContainer = document.querySelector('.sidebar-container'); + + // Initialize the overlay + const overlayClasses = ['hx-fixed', 'hx-inset-0', 'hx-z-10', 'hx-bg-black/80', 'dark:hx-bg-black/60']; + overlay.classList.add('hx-bg-transparent'); + overlay.classList.remove("hx-hidden", ...overlayClasses); + + function toggleMenu() { + // Toggle the hamburger menu + menu.querySelector('svg').classList.toggle('open'); + + // When the menu is open, we want to show the navigation sidebar + sidebarContainer.classList.toggle('max-md:[transform:translate3d(0,-100%,0)]'); + sidebarContainer.classList.toggle('max-md:[transform:translate3d(0,0,0)]'); + + // When the menu is open, we want to prevent the body from scrolling + document.body.classList.toggle('hx-overflow-hidden'); + document.body.classList.toggle('md:hx-overflow-auto'); + } + + function hideOverlay() { + // Hide the overlay + overlay.classList.remove(...overlayClasses); + overlay.classList.add('hx-bg-transparent'); + } + + menu.addEventListener('click', (e) => { + e.preventDefault(); + toggleMenu(); + + if (overlay.classList.contains('hx-bg-transparent')) { + // Show the overlay + overlay.classList.add(...overlayClasses); + overlay.classList.remove('hx-bg-transparent'); + } else { + // Hide the overlay + hideOverlay(); + } + }); + + overlay.addEventListener('click', (e) => { + e.preventDefault(); + toggleMenu(); + + // Hide the overlay + hideOverlay(); + }); + + // Select all anchor tags in the sidebar container + const sidebarLinks = sidebarContainer.querySelectorAll('a'); + + // Add click event listener to each anchor tag + sidebarLinks.forEach(link => { + link.addEventListener('click', (e) => { + // Check if the href attribute contains a hash symbol (links to a heading) + if (link.getAttribute('href') && link.getAttribute('href').startsWith('#')) { + // Only dismiss overlay on mobile view + if (window.innerWidth < 768) { + toggleMenu(); + hideOverlay(); + } + } + }); + }); +}); + +; +// Copy button for code blocks + +document.addEventListener('DOMContentLoaded', function () { + const getCopyIcon = () => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.innerHTML = ` + + `; + svg.setAttribute('fill', 'none'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('stroke', 'currentColor'); + svg.setAttribute('stroke-width', '2'); + return svg; + } + + const getSuccessIcon = () => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.innerHTML = ` + + `; + svg.setAttribute('fill', 'none'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('stroke', 'currentColor'); + svg.setAttribute('stroke-width', '2'); + return svg; + } + + document.querySelectorAll('.hextra-code-copy-btn').forEach(function (button) { + // Add copy and success icons + button.querySelector('.copy-icon')?.appendChild(getCopyIcon()); + button.querySelector('.success-icon')?.appendChild(getSuccessIcon()); + + // Add click event listener for copy button + button.addEventListener('click', function (e) { + e.preventDefault(); + // Get the code target + const target = button.parentElement.previousElementSibling; + let codeElement; + if (target.tagName === 'CODE') { + codeElement = target; + } else { + // Select the last code element in case line numbers are present + const codeElements = target.querySelectorAll('code'); + codeElement = codeElements[codeElements.length - 1]; + } + if (codeElement) { + let code = codeElement.innerText; + // Replace double newlines with single newlines in the innerText + // as each line inside has trailing newline '\n' + if ("lang" in codeElement.dataset) { + code = code.replace(/\n\n/g, '\n'); + } + navigator.clipboard.writeText(code).then(function () { + button.classList.add('copied'); + setTimeout(function () { + button.classList.remove('copied'); + }, 1000); + }).catch(function (err) { + console.error('Failed to copy text: ', err); + }); + } else { + console.error('Target element not found'); + } + }); + }); +}); + +; +document.querySelectorAll('.hextra-tabs-toggle').forEach(function (button) { + button.addEventListener('click', function (e) { + // set parent tabs to unselected + const tabs = Array.from(e.target.parentElement.querySelectorAll('.hextra-tabs-toggle')); + tabs.map(tab => tab.dataset.state = ''); + + // set current tab to selected + e.target.dataset.state = 'selected'; + + // set all panels to unselected + const panelsContainer = e.target.parentElement.parentElement.nextElementSibling; + Array.from(panelsContainer.children).forEach(function (panel) { + panel.dataset.state = ''; + }); + + const panelId = e.target.getAttribute('aria-controls'); + const panel = panelsContainer.querySelector(`#${panelId}`); + panel.dataset.state = 'selected'; + }); +}); + +; +(function () { + const languageSwitchers = document.querySelectorAll('.language-switcher'); + languageSwitchers.forEach((switcher) => { + switcher.addEventListener('click', (e) => { + e.preventDefault(); + switcher.dataset.state = switcher.dataset.state === 'open' ? 'closed' : 'open'; + const optionsElement = switcher.nextElementSibling; + optionsElement.classList.toggle('hx-hidden'); + + // Calculate position of language options element + const switcherRect = switcher.getBoundingClientRect(); + const translateY = switcherRect.top - window.innerHeight - 15; + optionsElement.style.transform = `translate3d(${switcherRect.left}px, ${translateY}px, 0)`; + optionsElement.style.minWidth = `${Math.max(switcherRect.width, 50)}px`; + }); + }); + + // Dismiss language switcher when clicking outside + document.addEventListener('click', (e) => { + if (e.target.closest('.language-switcher') === null) { + languageSwitchers.forEach((switcher) => { + switcher.dataset.state = 'closed'; + const optionsElement = switcher.nextElementSibling; + optionsElement.classList.add('hx-hidden'); + }); + } + }); +})(); + +; +// Script for filetree shortcode collapsing/expanding folders used in the theme +// ====================================================================== +document.addEventListener("DOMContentLoaded", function () { + const folders = document.querySelectorAll(".hextra-filetree-folder"); + folders.forEach(function (folder) { + folder.addEventListener("click", function () { + Array.from(folder.children).forEach(function (el) { + el.dataset.state = el.dataset.state === "open" ? "closed" : "open"; + }); + folder.nextElementSibling.dataset.state = folder.nextElementSibling.dataset.state === "open" ? "closed" : "open"; + }); + }); +}); + +; +document.addEventListener("DOMContentLoaded", function () { + scrollToActiveItem(); + enableCollapsibles(); +}); + +function enableCollapsibles() { + const buttons = document.querySelectorAll(".hextra-sidebar-collapsible-button"); + buttons.forEach(function (button) { + button.addEventListener("click", function (e) { + e.preventDefault(); + const list = button.parentElement.parentElement; + if (list) { + list.classList.toggle("open") + } + }); + }); +} + +function scrollToActiveItem() { + const sidebarScrollbar = document.querySelector("aside.sidebar-container > .hextra-scrollbar"); + const activeItems = document.querySelectorAll(".sidebar-active-item"); + const visibleActiveItem = Array.from(activeItems).find(function (activeItem) { + return activeItem.getBoundingClientRect().height > 0; + }); + + if (!visibleActiveItem) { + return; + } + + const yOffset = visibleActiveItem.clientHeight; + const yDistance = visibleActiveItem.getBoundingClientRect().top - sidebarScrollbar.getBoundingClientRect().top; + sidebarScrollbar.scrollTo({ + behavior: "instant", + top: yDistance - yOffset + }); +} + +; +// Back to top button + +document.addEventListener("DOMContentLoaded", function () { + const backToTop = document.querySelector("#backToTop"); + if (backToTop) { + document.addEventListener("scroll", (e) => { + if (window.scrollY > 300) { + backToTop.classList.remove("hx-opacity-0"); + } else { + backToTop.classList.add("hx-opacity-0"); + } + }); + } +}); + +function scrollUp() { + window.scroll({ + top: 0, + left: 0, + behavior: "smooth", + }); +} diff --git a/js/main.min.0942239592f0795dc7213dd1057fd229d7edc19c0ef1a285396eb292d9ca9842.js b/js/main.min.0942239592f0795dc7213dd1057fd229d7edc19c0ef1a285396eb292d9ca9842.js new file mode 100644 index 0000000..3ed1b78 --- /dev/null +++ b/js/main.min.0942239592f0795dc7213dd1057fd229d7edc19c0ef1a285396eb292d9ca9842.js @@ -0,0 +1,6 @@ +(function(){const t="system",e=document.querySelectorAll(".theme-toggle");localStorage.getItem("color-theme")==="dark"||!("color-theme"in localStorage)&&(window.matchMedia("(prefers-color-scheme: dark)").matches&&t==="system"||t==="dark")?e.forEach(e=>e.dataset.theme="dark"):e.forEach(e=>e.dataset.theme="light"),e.forEach(e=>{e.addEventListener("click",function(){localStorage.getItem("color-theme")?localStorage.getItem("color-theme")==="light"?(setDarkTheme(),localStorage.setItem("color-theme","dark")):(setLightTheme(),localStorage.setItem("color-theme","light")):document.documentElement.classList.contains("dark")?(setLightTheme(),localStorage.setItem("color-theme","light")):(setDarkTheme(),localStorage.setItem("color-theme","dark")),e.dataset.theme=document.documentElement.classList.contains("dark")?"dark":"light"})}),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",n=>{t==="system"&&!("color-theme"in localStorage)&&(n.matches?setDarkTheme():setLightTheme(),e.forEach(e=>e.dataset.theme=document.documentElement.classList.contains("dark")?"dark":"light"))})})(),document.addEventListener("DOMContentLoaded",function(){const i=document.querySelector(".hamburger-menu"),e=document.querySelector(".mobile-menu-overlay"),t=document.querySelector(".sidebar-container"),n=["hx-fixed","hx-inset-0","hx-z-10","hx-bg-black/80","dark:hx-bg-black/60"];e.classList.add("hx-bg-transparent"),e.classList.remove("hx-hidden",...n);function s(){i.querySelector("svg").classList.toggle("open"),t.classList.toggle("max-md:[transform:translate3d(0,-100%,0)]"),t.classList.toggle("max-md:[transform:translate3d(0,0,0)]"),document.body.classList.toggle("hx-overflow-hidden"),document.body.classList.toggle("md:hx-overflow-auto")}function o(){e.classList.remove(...n),e.classList.add("hx-bg-transparent")}i.addEventListener("click",t=>{t.preventDefault(),s(),e.classList.contains("hx-bg-transparent")?(e.classList.add(...n),e.classList.remove("hx-bg-transparent")):o()}),e.addEventListener("click",e=>{e.preventDefault(),s(),o()});const a=t.querySelectorAll("a");a.forEach(e=>{e.addEventListener("click",t=>{e.getAttribute("href")&&e.getAttribute("href").startsWith("#")&&window.innerWidth<768&&(s(),o())})})}),document.addEventListener("DOMContentLoaded",function(){const e=()=>{const e=document.createElementNS("http://www.w3.org/2000/svg","svg");return e.innerHTML=` + + `,e.setAttribute("fill","none"),e.setAttribute("viewBox","0 0 24 24"),e.setAttribute("stroke","currentColor"),e.setAttribute("stroke-width","2"),e},t=()=>{const e=document.createElementNS("http://www.w3.org/2000/svg","svg");return e.innerHTML=` + + `,e.setAttribute("fill","none"),e.setAttribute("viewBox","0 0 24 24"),e.setAttribute("stroke","currentColor"),e.setAttribute("stroke-width","2"),e};document.querySelectorAll(".hextra-code-copy-btn").forEach(function(n){n.querySelector(".copy-icon")?.appendChild(e()),n.querySelector(".success-icon")?.appendChild(t()),n.addEventListener("click",function(e){e.preventDefault();const s=n.parentElement.previousElementSibling;let t;if(s.tagName==="CODE")t=s;else{const e=s.querySelectorAll("code");t=e[e.length-1]}if(t){let e=t.innerText;"lang"in t.dataset&&(e=e.replace(/\n\n/g,` +`)),navigator.clipboard.writeText(e).then(function(){n.classList.add("copied"),setTimeout(function(){n.classList.remove("copied")},1e3)}).catch(function(e){console.error("Failed to copy text: ",e)})}else console.error("Target element not found")})})}),document.querySelectorAll(".hextra-tabs-toggle").forEach(function(e){e.addEventListener("click",function(e){const n=Array.from(e.target.parentElement.querySelectorAll(".hextra-tabs-toggle"));n.map(e=>e.dataset.state=""),e.target.dataset.state="selected";const t=e.target.parentElement.parentElement.nextElementSibling;Array.from(t.children).forEach(function(e){e.dataset.state=""});const s=e.target.getAttribute("aria-controls"),o=t.querySelector(`#${s}`);o.dataset.state="selected"})}),function(){const e=document.querySelectorAll(".language-switcher");e.forEach(e=>{e.addEventListener("click",t=>{t.preventDefault(),e.dataset.state=e.dataset.state==="open"?"closed":"open";const n=e.nextElementSibling;n.classList.toggle("hx-hidden");const s=e.getBoundingClientRect(),o=s.top-window.innerHeight-15;n.style.transform=`translate3d(${s.left}px, ${o}px, 0)`,n.style.minWidth=`${Math.max(s.width,50)}px`})}),document.addEventListener("click",t=>{t.target.closest(".language-switcher")===null&&e.forEach(e=>{e.dataset.state="closed";const t=e.nextElementSibling;t.classList.add("hx-hidden")})})}(),document.addEventListener("DOMContentLoaded",function(){const e=document.querySelectorAll(".hextra-filetree-folder");e.forEach(function(e){e.addEventListener("click",function(){Array.from(e.children).forEach(function(e){e.dataset.state=e.dataset.state==="open"?"closed":"open"}),e.nextElementSibling.dataset.state=e.nextElementSibling.dataset.state==="open"?"closed":"open"})})}),document.addEventListener("DOMContentLoaded",function(){scrollToActiveItem(),enableCollapsibles()});function enableCollapsibles(){const e=document.querySelectorAll(".hextra-sidebar-collapsible-button");e.forEach(function(e){e.addEventListener("click",function(t){t.preventDefault();const n=e.parentElement.parentElement;n&&n.classList.toggle("open")})})}function scrollToActiveItem(){const t=document.querySelector("aside.sidebar-container > .hextra-scrollbar"),n=document.querySelectorAll(".sidebar-active-item"),e=Array.from(n).find(function(e){return e.getBoundingClientRect().height>0});if(!e)return;const s=e.clientHeight,o=e.getBoundingClientRect().top-t.getBoundingClientRect().top;t.scrollTo({behavior:"instant",top:o-s})}document.addEventListener("DOMContentLoaded",function(){const e=document.querySelector("#backToTop");e&&document.addEventListener("scroll",t=>{window.scrollY>300?e.classList.remove("hx-opacity-0"):e.classList.add("hx-opacity-0")})});function scrollUp(){window.scroll({top:0,left:0,behavior:"smooth"})} \ No newline at end of file diff --git a/lib/flexsearch/flexsearch.bundle.min.0425860527cc9968f9f049421c7a56b39327d475e2e3a8f550416be3a9134327.js b/lib/flexsearch/flexsearch.bundle.min.0425860527cc9968f9f049421c7a56b39327d475e2e3a8f550416be3a9134327.js new file mode 100644 index 0000000..4ebf76e --- /dev/null +++ b/lib/flexsearch/flexsearch.bundle.min.0425860527cc9968f9f049421c7a56b39327d475e2e3a8f550416be3a9134327.js @@ -0,0 +1,39 @@ +/** + * Skipped minification because the original files appears to be already minified. + * Original file: /npm/flexsearch@0.7.31/dist/flexsearch.bundle.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/**! + * FlexSearch.js v0.7.31 (Bundle) + * Copyright 2018-2022 Nextapps GmbH + * Author: Thomas Wilkerling + * Licence: Apache-2.0 + * https://github.com/nextapps-de/flexsearch + */ +(function _f(self){'use strict';try{if(module)self=module}catch(e){}self._factory=_f;var t;function u(a){return"undefined"!==typeof a?a:!0}function aa(a){const b=Array(a);for(let c=0;c=this.B&&(w||!n[l])){var f=L(q,d,r),g="";switch(this.G){case "full":if(2f;h--)if(h-f>=this.B){var k=L(q,d,r,e,f);g=l.substring(f,h);M(this,n,g,k,a,c)}break}case "reverse":if(1=this.B&&M(this,n, +g,L(q,d,r,e,h),a,c);g=""}case "forward":if(1=this.B&&M(this,n,g,f,a,c);break}default:if(this.C&&(f=Math.min(f/this.C(b,l,r)|0,q-1)),M(this,n,l,f,a,c),w&&1=this.B&&!e[l]){e[l]=1;const p=this.l&&l>f;M(this,m,p?f:l,L(g+(d/2>g?0:1),d,r,h-1,k-1),a,c,p?l:f)}}}}this.m||(this.register[a]=1)}}return this}; +function L(a,b,c,d,e){return c&&1=this.B&&!c[q])if(this.s||f||this.map[q])k[w++]=q,c[q]=1;else return d;a=k;e=a.length}if(!e)return d;b||(b=100);h=this.depth&&1=d)))break;if(n){if(f)return ta(k,d,0);b[b.length]=k;return}}return!c&&k}function ta(a,b,c){a=1===a.length?a[0]:[].concat.apply([],a);return c||a.length>b?a.slice(c,c+b):a} +function ua(a,b,c,d){c?(d=d&&b>c,a=(a=a[d?b:c])&&a[d?c:b]):a=a[b];return a}t.contain=function(a){return!!this.register[a]};t.update=function(a,b){return this.remove(a).add(a,b)}; +t.remove=function(a,b){const c=this.register[a];if(c){if(this.m)for(let d=0,e;db||c)e=e.slice(c,c+b);d&&(e=za.call(this,e));return{tag:a,result:e}}}function za(a){const b=Array(a.length);for(let c=0,d;c + + + /docs/form/mount/ + + /docs/form/bind/ + + /docs/fields/input/hidden/ + + /docs/installation/ + + /docs/form/ + + /docs/fields/ + + /docs/fields/input/ + + /docs/constraints/ + + /docs/fields/button/ + + /docs/fields/textarea/ + + /docs/rendering/ + + /docs/rendering/theming/ + + /categories/ + + /docs/fields/input/date/ + + /docs/fields/input/datetime/ + + /docs/ + + /docs/fields/input/mail/ + + /docs/fields/input/number/ + + /docs/fields/input/password/ + + /docs/fields/input/range/ + + /tags/ + + /docs/fields/input/text/ + + /docs/fields/input/time/ + + / + + diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 0000000..e4de4c8 --- /dev/null +++ b/tags/index.html @@ -0,0 +1,374 @@ + + + + + + + + + + + + +Tags – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + + + + +
      +
      +
      +

      Tags

      +
      +
      + +
      +
      + +
      +
      +
      +
      + +
      +

      + + + + + + diff --git a/tags/index.xml b/tags/index.xml new file mode 100644 index 0000000..9203f75 --- /dev/null +++ b/tags/index.xml @@ -0,0 +1,18 @@ + + + deblan/go-form – Tags + /tags/ + Recent content in Tags on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + From 88ec77ac0b6d4aebc6ff51820f7c881b760882ae Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 21 Jul 2025 07:20:05 +0000 Subject: [PATCH 028/117] Build doc --- categories/index.html | 80 +++++------ categories/index.xml | 4 +- docs/constraints/index.html | 122 ++++++++-------- docs/constraints/index.xml | 4 +- docs/fields/button/index.html | 126 ++++++++--------- docs/fields/index.html | 122 ++++++++-------- docs/fields/index.xml | 16 +-- docs/fields/input/date/index.html | 130 +++++++++--------- docs/fields/input/datetime/index.html | 130 +++++++++--------- docs/fields/input/hidden/index.html | 128 ++++++++--------- docs/fields/input/index.html | 124 ++++++++--------- docs/fields/input/index.xml | 40 +++--- docs/fields/input/mail/index.html | 130 +++++++++--------- docs/fields/input/number/index.html | 130 +++++++++--------- docs/fields/input/password/index.html | 130 +++++++++--------- docs/fields/input/range/index.html | 130 +++++++++--------- docs/fields/input/text/index.html | 130 +++++++++--------- docs/fields/input/time/index.html | 128 ++++++++--------- docs/fields/textarea/index.html | 126 ++++++++--------- docs/form/bind/index.html | 126 ++++++++--------- docs/form/index.html | 122 ++++++++-------- docs/form/index.xml | 12 +- docs/form/mount/index.html | 126 ++++++++--------- docs/index.html | 120 ++++++++-------- docs/index.xml | 64 ++++----- docs/installation/index.html | 122 ++++++++-------- docs/rendering/index.html | 122 ++++++++-------- docs/rendering/index.xml | 8 +- docs/rendering/theming/index.html | 124 ++++++++--------- en.search-data.json | 2 +- ...f8f25d872bed2eae488d8626311a4dcf87bff9b.js | 8 ++ index.html | 80 +++++------ sitemap.xml | 48 +++---- tags/index.html | 80 +++++------ tags/index.xml | 4 +- 35 files changed, 1553 insertions(+), 1545 deletions(-) create mode 100644 en.search.min.fe55059febf424bd0ddfb7f9af8f25d872bed2eae488d8626311a4dcf87bff9b.js diff --git a/categories/index.html b/categories/index.html index b5edba6..c35083a 100644 --- a/categories/index.html +++ b/categories/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Categories – deblan/go-form - + - + @@ -24,12 +24,12 @@ - - + + - + - - + + + diff --git a/categories/index.xml b/categories/index.xml index fb03ab1..69093ec 100644 --- a/categories/index.xml +++ b/categories/index.xml @@ -1,12 +1,12 @@ deblan/go-form – Categories - /categories/ + https://deblan.gitnet.page/go-form/categories/ Recent content in Categories on deblan/go-form Hugo -- gohugo.io en-us - + diff --git a/docs/constraints/index.html b/docs/constraints/index.html index 7acb3cb..4bba1b6 100644 --- a/docs/constraints/index.html +++ b/docs/constraints/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Constraints – deblan/go-form - + - + @@ -24,12 +24,12 @@ - - + + - + - - + + + diff --git a/docs/constraints/index.xml b/docs/constraints/index.xml index d872a21..279659e 100644 --- a/docs/constraints/index.xml +++ b/docs/constraints/index.xml @@ -1,12 +1,12 @@ deblan/go-form – Constraints - /docs/constraints/ + https://deblan.gitnet.page/go-form/docs/constraints/ Recent content in Constraints on deblan/go-form Hugo -- gohugo.io en-us - + diff --git a/docs/fields/button/index.html b/docs/fields/button/index.html index e9def52..708f573 100644 --- a/docs/fields/button/index.html +++ b/docs/fields/button/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + deblan/go-form - + - + @@ -26,12 +26,12 @@ - - + + - + - - + + + diff --git a/docs/fields/index.html b/docs/fields/index.html index 26f7f5c..e677f08 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Input – deblan/go-form - + - + @@ -24,12 +24,12 @@ - - + + - + - - + + + diff --git a/docs/fields/index.xml b/docs/fields/index.xml index 8f76540..b4fff97 100644 --- a/docs/fields/index.xml +++ b/docs/fields/index.xml @@ -1,12 +1,12 @@ deblan/go-form – Input - /docs/fields/ + https://deblan.gitnet.page/go-form/docs/fields/ Recent content in Input on deblan/go-form Hugo -- gohugo.io en-us - + @@ -16,10 +16,10 @@ Input - /docs/fields/input/ + https://deblan.gitnet.page/go-form/docs/fields/input/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/ + https://deblan.gitnet.page/go-form/docs/fields/input/ @@ -34,10 +34,10 @@ - /docs/fields/button/ + https://deblan.gitnet.page/go-form/docs/fields/button/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/button/ + https://deblan.gitnet.page/go-form/docs/fields/button/ @@ -47,10 +47,10 @@ - /docs/fields/textarea/ + https://deblan.gitnet.page/go-form/docs/fields/textarea/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/textarea/ + https://deblan.gitnet.page/go-form/docs/fields/textarea/ diff --git a/docs/fields/input/date/index.html b/docs/fields/input/date/index.html index 198f69a..56465f1 100644 --- a/docs/fields/input/date/index.html +++ b/docs/fields/input/date/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Date – deblan/go-form - + - + @@ -26,12 +26,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/datetime/index.html b/docs/fields/input/datetime/index.html index ce9c8f7..5d3ea65 100644 --- a/docs/fields/input/datetime/index.html +++ b/docs/fields/input/datetime/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Datetime – deblan/go-form - + - + @@ -26,12 +26,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/hidden/index.html b/docs/fields/input/hidden/index.html index 6c8c2e4..4df763c 100644 --- a/docs/fields/input/hidden/index.html +++ b/docs/fields/input/hidden/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Hidden – deblan/go-form - + - + @@ -26,12 +26,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/index.html b/docs/fields/input/index.html index e8bef98..c2b84c2 100644 --- a/docs/fields/input/index.html +++ b/docs/fields/input/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Input – deblan/go-form - + - + @@ -25,12 +25,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/index.xml b/docs/fields/input/index.xml index 793efa4..4eebc2a 100644 --- a/docs/fields/input/index.xml +++ b/docs/fields/input/index.xml @@ -1,12 +1,12 @@ deblan/go-form – Input - /docs/fields/input/ + https://deblan.gitnet.page/go-form/docs/fields/input/ Recent content in Input on deblan/go-form Hugo -- gohugo.io en-us - + @@ -16,10 +16,10 @@ Hidden - /docs/fields/input/hidden/ + https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/hidden/ + https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ @@ -29,10 +29,10 @@ Date - /docs/fields/input/date/ + https://deblan.gitnet.page/go-form/docs/fields/input/date/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/date/ + https://deblan.gitnet.page/go-form/docs/fields/input/date/ @@ -42,10 +42,10 @@ Datetime - /docs/fields/input/datetime/ + https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/datetime/ + https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ @@ -55,10 +55,10 @@ Mail - /docs/fields/input/mail/ + https://deblan.gitnet.page/go-form/docs/fields/input/mail/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/mail/ + https://deblan.gitnet.page/go-form/docs/fields/input/mail/ @@ -73,10 +73,10 @@ Number - /docs/fields/input/number/ + https://deblan.gitnet.page/go-form/docs/fields/input/number/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/number/ + https://deblan.gitnet.page/go-form/docs/fields/input/number/ @@ -91,10 +91,10 @@ Password - /docs/fields/input/password/ + https://deblan.gitnet.page/go-form/docs/fields/input/password/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/password/ + https://deblan.gitnet.page/go-form/docs/fields/input/password/ @@ -109,10 +109,10 @@ Range - /docs/fields/input/range/ + https://deblan.gitnet.page/go-form/docs/fields/input/range/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/range/ + https://deblan.gitnet.page/go-form/docs/fields/input/range/ @@ -127,10 +127,10 @@ Text - /docs/fields/input/text/ + https://deblan.gitnet.page/go-form/docs/fields/input/text/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/text/ + https://deblan.gitnet.page/go-form/docs/fields/input/text/ @@ -145,10 +145,10 @@ Time - /docs/fields/input/time/ + https://deblan.gitnet.page/go-form/docs/fields/input/time/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/time/ + https://deblan.gitnet.page/go-form/docs/fields/input/time/ diff --git a/docs/fields/input/mail/index.html b/docs/fields/input/mail/index.html index 948442c..e164cd7 100644 --- a/docs/fields/input/mail/index.html +++ b/docs/fields/input/mail/index.html @@ -3,15 +3,15 @@ - - - - - - - + + + + + + + Mail – deblan/go-form - + - + @@ -31,12 +31,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/number/index.html b/docs/fields/input/number/index.html index d3affcc..21cb874 100644 --- a/docs/fields/input/number/index.html +++ b/docs/fields/input/number/index.html @@ -3,15 +3,15 @@ - - - - - - - + + + + + + + Number – deblan/go-form - + - + @@ -31,12 +31,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/password/index.html b/docs/fields/input/password/index.html index fafd30a..5e49287 100644 --- a/docs/fields/input/password/index.html +++ b/docs/fields/input/password/index.html @@ -3,15 +3,15 @@ - - - - - - - + + + + + + + Password – deblan/go-form - + - + @@ -31,12 +31,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/range/index.html b/docs/fields/input/range/index.html index a6d0c57..7445c23 100644 --- a/docs/fields/input/range/index.html +++ b/docs/fields/input/range/index.html @@ -3,15 +3,15 @@ - - - - - - - + + + + + + + Range – deblan/go-form - + - + @@ -31,12 +31,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/text/index.html b/docs/fields/input/text/index.html index acc0810..a45d0fe 100644 --- a/docs/fields/input/text/index.html +++ b/docs/fields/input/text/index.html @@ -3,15 +3,15 @@ - - - - - - - + + + + + + + Text – deblan/go-form - + - + @@ -31,12 +31,12 @@ - - + + - + - - + + + diff --git a/docs/fields/input/time/index.html b/docs/fields/input/time/index.html index e7d6797..cae2049 100644 --- a/docs/fields/input/time/index.html +++ b/docs/fields/input/time/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Time – deblan/go-form - + - + @@ -26,12 +26,12 @@ - - + + - + - - + + + diff --git a/docs/fields/textarea/index.html b/docs/fields/textarea/index.html index 74ac5ef..f32158b 100644 --- a/docs/fields/textarea/index.html +++ b/docs/fields/textarea/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + deblan/go-form - + - + @@ -26,12 +26,12 @@ - - + + - + - - + + + diff --git a/docs/form/bind/index.html b/docs/form/bind/index.html index 1cf1d29..238536a 100644 --- a/docs/form/bind/index.html +++ b/docs/form/bind/index.html @@ -3,15 +3,15 @@ - - - - - - - + + + + + + + Bind – deblan/go-form - + - + @@ -69,12 +69,12 @@ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - - + + - + - - + + + diff --git a/docs/form/index.html b/docs/form/index.html index 94b3924..ede746e 100644 --- a/docs/form/index.html +++ b/docs/form/index.html @@ -3,21 +3,21 @@ - - - - - - - + + + + + + + Form – deblan/go-form +Fields Options Method Action Import import ( "net/http" "gitnet.fr/deblan/go-form/form" ) Usage // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // <form method="POST" ...> myForm.WithMethod(http.MethodPost) // Define the action // <form action="/" ...> myForm.WithAction("/") // Set a name myForm.WithName("myForm") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes." /> - + - - + + - + - - + + + diff --git a/docs/form/index.xml b/docs/form/index.xml index 1ce3e96..66a0932 100644 --- a/docs/form/index.xml +++ b/docs/form/index.xml @@ -1,12 +1,12 @@ deblan/go-form – Form - /docs/form/ + https://deblan.gitnet.page/go-form/docs/form/ Recent content in Form on deblan/go-form Hugo -- gohugo.io en-us - + @@ -16,10 +16,10 @@ Mount - /docs/form/mount/ + https://deblan.gitnet.page/go-form/docs/form/mount/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/form/mount/ + https://deblan.gitnet.page/go-form/docs/form/mount/ @@ -52,10 +52,10 @@ Bind - /docs/form/bind/ + https://deblan.gitnet.page/go-form/docs/form/bind/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/form/bind/ + https://deblan.gitnet.page/go-form/docs/form/bind/ diff --git a/docs/form/mount/index.html b/docs/form/mount/index.html index 2024828..9b3321f 100644 --- a/docs/form/mount/index.html +++ b/docs/form/mount/index.html @@ -3,15 +3,15 @@ - - - - - - - + + + + + + + Mount – deblan/go-form - + - + @@ -46,12 +46,12 @@ myForm.Mount(alice) - - + + - + - - + + + diff --git a/docs/index.html b/docs/index.html index 7be1e4a..e1dc575 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,22 +3,22 @@ - - - - - - - + + + + + + + Introduction – deblan/go-form +A form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes " /> - + - - + + - + - - + + + diff --git a/docs/index.xml b/docs/index.xml index 967ef56..d85cb08 100644 --- a/docs/index.xml +++ b/docs/index.xml @@ -1,12 +1,12 @@ deblan/go-form – Introduction - /docs/ + https://deblan.gitnet.page/go-form/docs/ Recent content in Introduction on deblan/go-form Hugo -- gohugo.io en-us - + @@ -16,10 +16,10 @@ Mount - /docs/form/mount/ + https://deblan.gitnet.page/go-form/docs/form/mount/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/form/mount/ + https://deblan.gitnet.page/go-form/docs/form/mount/ @@ -52,10 +52,10 @@ Bind - /docs/form/bind/ + https://deblan.gitnet.page/go-form/docs/form/bind/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/form/bind/ + https://deblan.gitnet.page/go-form/docs/form/bind/ @@ -111,10 +111,10 @@ Hidden - /docs/fields/input/hidden/ + https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/hidden/ + https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ @@ -124,10 +124,10 @@ Installation - /docs/installation/ + https://deblan.gitnet.page/go-form/docs/installation/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/installation/ + https://deblan.gitnet.page/go-form/docs/installation/ @@ -137,10 +137,10 @@ - /docs/fields/button/ + https://deblan.gitnet.page/go-form/docs/fields/button/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/button/ + https://deblan.gitnet.page/go-form/docs/fields/button/ @@ -150,10 +150,10 @@ - /docs/fields/textarea/ + https://deblan.gitnet.page/go-form/docs/fields/textarea/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/textarea/ + https://deblan.gitnet.page/go-form/docs/fields/textarea/ @@ -163,10 +163,10 @@ - /docs/rendering/theming/ + https://deblan.gitnet.page/go-form/docs/rendering/theming/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/rendering/theming/ + https://deblan.gitnet.page/go-form/docs/rendering/theming/ @@ -176,10 +176,10 @@ Date - /docs/fields/input/date/ + https://deblan.gitnet.page/go-form/docs/fields/input/date/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/date/ + https://deblan.gitnet.page/go-form/docs/fields/input/date/ @@ -189,10 +189,10 @@ Datetime - /docs/fields/input/datetime/ + https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/datetime/ + https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ @@ -202,10 +202,10 @@ Mail - /docs/fields/input/mail/ + https://deblan.gitnet.page/go-form/docs/fields/input/mail/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/mail/ + https://deblan.gitnet.page/go-form/docs/fields/input/mail/ @@ -220,10 +220,10 @@ Number - /docs/fields/input/number/ + https://deblan.gitnet.page/go-form/docs/fields/input/number/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/number/ + https://deblan.gitnet.page/go-form/docs/fields/input/number/ @@ -238,10 +238,10 @@ Password - /docs/fields/input/password/ + https://deblan.gitnet.page/go-form/docs/fields/input/password/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/password/ + https://deblan.gitnet.page/go-form/docs/fields/input/password/ @@ -256,10 +256,10 @@ Range - /docs/fields/input/range/ + https://deblan.gitnet.page/go-form/docs/fields/input/range/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/range/ + https://deblan.gitnet.page/go-form/docs/fields/input/range/ @@ -274,10 +274,10 @@ Text - /docs/fields/input/text/ + https://deblan.gitnet.page/go-form/docs/fields/input/text/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/text/ + https://deblan.gitnet.page/go-form/docs/fields/input/text/ @@ -292,10 +292,10 @@ Time - /docs/fields/input/time/ + https://deblan.gitnet.page/go-form/docs/fields/input/time/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/fields/input/time/ + https://deblan.gitnet.page/go-form/docs/fields/input/time/ diff --git a/docs/installation/index.html b/docs/installation/index.html index 9f6506f..432c549 100644 --- a/docs/installation/index.html +++ b/docs/installation/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Installation – deblan/go-form - + - + @@ -27,12 +27,12 @@ - - + + - + - - + + + diff --git a/docs/rendering/index.html b/docs/rendering/index.html index 220abb1..1957ed8 100644 --- a/docs/rendering/index.html +++ b/docs/rendering/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + deblan/go-form - + - + @@ -24,12 +24,12 @@ - - + + - + - - + + + diff --git a/docs/rendering/index.xml b/docs/rendering/index.xml index 761ec73..fb25960 100644 --- a/docs/rendering/index.xml +++ b/docs/rendering/index.xml @@ -1,12 +1,12 @@ deblan/go-form – - /docs/rendering/ + https://deblan.gitnet.page/go-form/docs/rendering/ Recent content on deblan/go-form Hugo -- gohugo.io en-us - + @@ -16,10 +16,10 @@ - /docs/rendering/theming/ + https://deblan.gitnet.page/go-form/docs/rendering/theming/ Mon, 01 Jan 0001 00:00:00 +0000 - /docs/rendering/theming/ + https://deblan.gitnet.page/go-form/docs/rendering/theming/ diff --git a/docs/rendering/theming/index.html b/docs/rendering/theming/index.html index 8b7ff19..4c41076 100644 --- a/docs/rendering/theming/index.html +++ b/docs/rendering/theming/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + deblan/go-form - + - + @@ -26,12 +26,12 @@ - - + + - + - - + + + diff --git a/en.search-data.json b/en.search-data.json index 6a989d0..ee2f8ff 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes ","questions-or-feedback#Questions or Feedback?":" fmt.Sprintf(\"foo\") "},"title":"Documentation"},"/docs/fields/input/":{"data":{"":" ok "},"title":"Input"},"/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/docs/fields/input/text/":{"data":{"":" ok "},"title":"Text"},"/docs/form/":{"data":{"":"A form is a struct containing:\nFields Options Method Action ","attributes#Attributes":"Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) ","import#Import":" import ( \"net/http\" \"gitnet.fr/deblan/go-form/form\" ) ","usage#Usage":" // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() "},"title":"Form"},"/docs/form/bind/":{"data":{"":" import ( \"net/http\" \"gitnet.fr/deblan/go-form/form\" ) type Person struct { Name string Age int } func createForm() *form.Form { f := form.NewForm() // do stuff return f } http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { alice := Person{ Name: \"Alice\", Age: 42, } myForm := createForm() if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Bind"},"/docs/form/mount/":{"data":{"":" type Person struct { Name string Age int } alice := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(alice) "},"title":"Mount"},"/docs/installation/":{"data":{"installation#Installation":"Installation"},"title":"Installation"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes ","questions-or-feedback#Questions or Feedback?":" fmt.Sprintf(\"foo\") "},"title":"Documentation"},"/go-form/docs/fields/input/":{"data":{"":" ok "},"title":"Input"},"/go-form/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/go-form/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/go-form/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/go-form/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/go-form/docs/fields/input/text/":{"data":{"":" ok "},"title":"Text"},"/go-form/docs/form/":{"data":{"":"A form is a struct containing:\nFields Options Method Action ","attributes#Attributes":"Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) ","import#Import":" import ( \"net/http\" \"gitnet.fr/deblan/go-form/form\" ) ","usage#Usage":" // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() "},"title":"Form"},"/go-form/docs/form/bind/":{"data":{"":" import ( \"net/http\" \"gitnet.fr/deblan/go-form/form\" ) type Person struct { Name string Age int } func createForm() *form.Form { f := form.NewForm() // do stuff return f } http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { alice := Person{ Name: \"Alice\", Age: 42, } myForm := createForm() if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Bind"},"/go-form/docs/form/mount/":{"data":{"":" type Person struct { Name string Age int } alice := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(alice) "},"title":"Mount"},"/go-form/docs/installation/":{"data":{"installation#Installation":"Installation"},"title":"Installation"}} \ No newline at end of file diff --git a/en.search.min.fe55059febf424bd0ddfb7f9af8f25d872bed2eae488d8626311a4dcf87bff9b.js b/en.search.min.fe55059febf424bd0ddfb7f9af8f25d872bed2eae488d8626311a4dcf87bff9b.js new file mode 100644 index 0000000..8f89402 --- /dev/null +++ b/en.search.min.fe55059febf424bd0ddfb7f9af8f25d872bed2eae488d8626311a4dcf87bff9b.js @@ -0,0 +1,8 @@ +document.addEventListener("DOMContentLoaded",function(){if(/iPad|iPhone|Macintosh/.test(navigator.userAgent)){const e=document.querySelectorAll(".search-wrapper kbd");e.forEach(e=>{e.innerHTML='K'})}}),function(){const h="/go-form/en.search-data.json",u=document.querySelectorAll(".search-input");for(const e of u)e.addEventListener("focus",o),e.addEventListener("keyup",p),e.addEventListener("keydown",n),e.addEventListener("input",m);const d=document.querySelectorAll(".search-wrapper kbd");function a(e){d.forEach(t=>{t.style.opacity=e})}function m(e){const t=e.target.value.length>0?0:100;a(t)}function e(){const e=Array.from(document.querySelectorAll(".search-wrapper")).filter(e=>e.clientHeight>0);return e.length===1?{wrapper:e[0],inputElement:e[0].querySelector(".search-input"),resultsElement:e[0].querySelector(".search-results")}:0[0]}const l=["input","select","button","textarea"];document.addEventListener("keydown",function(t){const{inputElement:n}=e();if(!n)return;const s=document.activeElement,o=s&&s.tagName;if(n===s||!o||l.includes(o)||s&&s.isContentEditable)return;t.key==="/"||t.key==="k"&&(t.metaKey||t.ctrlKey)?(t.preventDefault(),n.focus()):t.key==="Escape"&&n.value&&n.blur()}),document.addEventListener("mousedown",function(n){const{inputElement:o,resultsElement:s}=e();if(!o||!s)return;n.target!==o&&n.target!==s&&!s.contains(n.target)&&(a(100),t())});function r(){const{resultsElement:n}=e();if(!n)return{result:0[0],index:-1};const t=n.querySelector(".active");if(!t)return{result:0[0],index:-1};const s=parseInt(t.dataset.index,10);return{result:t,index:s}}function s(t){const{resultsElement:s}=e();if(!s)return;const{result:o}=r();o&&o.classList.remove("active");const n=s.querySelector(`[data-index="${t}"]`);n&&(n.classList.add("active"),n.focus())}function c(){const{resultsElement:t}=e();return t?t.dataset.count:0}function i(){const{inputElement:n}=e();if(!n)return;t(),n.value="",n.blur()}function t(){const{resultsElement:t}=e();if(!t)return;t.classList.add("hx-hidden")}function n(n){const{inputElement:o}=e();if(!o)return;const d=c(),{result:l,index:a}=r();switch(n.key){case"ArrowUp":n.preventDefault(),a>0&&s(a-1);break;case"ArrowDown":n.preventDefault(),a+1{const e=document.documentElement.lang||"en";return e.startsWith("zh")||e.startsWith("ja")||e.startsWith("ko")},i=e=>e.replace(/[\x00-\x7F]/g,"").split(""),a=e=>(""+e).toLocaleLowerCase().split(/[\p{Z}\p{S}\p{P}\p{C}]+/u),s=o()?i:a;window.pageIndex=new FlexSearch.Document({tokenize:n,encode:s,cache:100,document:{id:"id",store:["title","crumb"],index:"content"}}),window.sectionIndex=new FlexSearch.Document({tokenize:n,encode:s,cache:100,document:{id:"id",store:["title","content","url","display","crumb"],index:"content",tag:"pageId"}});const r=await fetch(h),e=await r.json();let t=0;for(const n in e){let a="";++t;const i=n.split("/").filter(e=>e!=""&&!e.startsWith("#"));let s="",o="/";for(let t=0;te).join(" ")),s+=a,t ")}for(const c in e[n].data){const[l,u]=c.split("#"),o=n.trimEnd("/")+(l?"#"+l:""),i=u||e[n].title,d=e[n].data[c]||"",r=d.split(` +`).filter(Boolean);sectionIndex.add({id:o,url:o,title:i,crumb:s,pageId:`page_${t}`,content:i,...r[0]&&{display:r[0]}});for(let e=0;ee._page_rk===t._page_rk?e._section_rk-t._section_rk:s[e._page_rk]!==s[t._page_rk]?s[t._page_rk]-s[e._page_rk]:e._page_rk-t._page_rk).map(e=>({id:`${e._page_rk}_${e._section_rk}`,route:e.route,prefix:e.prefix,children:e.children}));g(c,i)}function g(t,s){const{resultsElement:o}=e();if(!o)return;if(!t.length){o.innerHTML=`No results found.`;return}function r(e,t){const n=t.replace(/[-\\^$*+?.()|[\]{}]/g,"\\$&"),s=new RegExp(n,"gi");return e.replace(s,e=>`${e}`)}function c(e){const t=document.createElement("div");return t.innerHTML=e.trim(),t.firstChild}function l(e){const t=e.target.closest("a");if(t){const e=o.querySelector("a.active");e&&e.classList.remove("active"),t.classList.add("active")}}const a=document.createDocumentFragment();for(let o=0;o${e.prefix}`));let d=c(` +
    • + +
      `+r(e.children.title,s)+`
      `+(e.children.content?`
      `+r(e.children.content,s)+`
      `:"")+` +
      +
    • `);d.addEventListener("mousemove",l),d.addEventListener("keydown",n),d.querySelector("a").addEventListener("click",i),a.appendChild(d)}o.appendChild(a),o.dataset.count=t.length}}() \ No newline at end of file diff --git a/index.html b/index.html index 8886d90..876ad1a 100644 --- a/index.html +++ b/index.html @@ -4,20 +4,20 @@ - - - - - - - + + + + + + + deblan/go-form - + - + @@ -26,12 +26,12 @@ - - + + - + - - + + + diff --git a/sitemap.xml b/sitemap.xml index 9a0fa93..58b15cb 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,52 +2,52 @@ - /docs/form/mount/ + https://deblan.gitnet.page/go-form/docs/form/mount/ - /docs/form/bind/ + https://deblan.gitnet.page/go-form/docs/form/bind/ - /docs/fields/input/hidden/ + https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ - /docs/installation/ + https://deblan.gitnet.page/go-form/docs/installation/ - /docs/form/ + https://deblan.gitnet.page/go-form/docs/form/ - /docs/fields/ + https://deblan.gitnet.page/go-form/docs/fields/ - /docs/fields/input/ + https://deblan.gitnet.page/go-form/docs/fields/input/ - /docs/constraints/ + https://deblan.gitnet.page/go-form/docs/constraints/ - /docs/fields/button/ + https://deblan.gitnet.page/go-form/docs/fields/button/ - /docs/fields/textarea/ + https://deblan.gitnet.page/go-form/docs/fields/textarea/ - /docs/rendering/ + https://deblan.gitnet.page/go-form/docs/rendering/ - /docs/rendering/theming/ + https://deblan.gitnet.page/go-form/docs/rendering/theming/ - /categories/ + https://deblan.gitnet.page/go-form/categories/ - /docs/fields/input/date/ + https://deblan.gitnet.page/go-form/docs/fields/input/date/ - /docs/fields/input/datetime/ + https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ - /docs/ + https://deblan.gitnet.page/go-form/docs/ - /docs/fields/input/mail/ + https://deblan.gitnet.page/go-form/docs/fields/input/mail/ - /docs/fields/input/number/ + https://deblan.gitnet.page/go-form/docs/fields/input/number/ - /docs/fields/input/password/ + https://deblan.gitnet.page/go-form/docs/fields/input/password/ - /docs/fields/input/range/ + https://deblan.gitnet.page/go-form/docs/fields/input/range/ - /tags/ + https://deblan.gitnet.page/go-form/tags/ - /docs/fields/input/text/ + https://deblan.gitnet.page/go-form/docs/fields/input/text/ - /docs/fields/input/time/ + https://deblan.gitnet.page/go-form/docs/fields/input/time/ - / + https://deblan.gitnet.page/go-form/ diff --git a/tags/index.html b/tags/index.html index e4de4c8..e60460f 100644 --- a/tags/index.html +++ b/tags/index.html @@ -3,20 +3,20 @@ - - - - - - - + + + + + + + Tags – deblan/go-form - + - + @@ -24,12 +24,12 @@ - - + + - + - - + + + diff --git a/tags/index.xml b/tags/index.xml index 9203f75..1df074e 100644 --- a/tags/index.xml +++ b/tags/index.xml @@ -1,12 +1,12 @@ deblan/go-form – Tags - /tags/ + https://deblan.gitnet.page/go-form/tags/ Recent content in Tags on deblan/go-form Hugo -- gohugo.io en-us - + From 8b28deb21e0dc9d226a98302016cf89fdb7dc100 Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 22 Jul 2025 04:55:55 +0000 Subject: [PATCH 029/117] Build doc --- css/custom.css => .hugo_build.lock | 0 categories/index.html | 80 +- css/compiled/main.css | 3637 ----------------- docs/01-introduction/index.html | 328 -- docs/02-installation/index.html | 252 -- docs/button/index.html | 467 --- docs/constraints/index.html | 158 +- docs/field/date/date/index.html | 464 --- docs/field/date/datetime/index.html | 468 --- docs/field/date/index.html | 455 --- docs/field/date/index.xml | 57 - docs/field/date/time/index.html | 464 --- docs/field/index.html | 452 -- docs/field/index.xml | 49 - docs/field/input/hidden/index.html | 464 --- docs/field/input/index.html | 461 --- docs/field/input/index.xml | 121 - docs/field/input/mail/index.html | 478 --- docs/field/input/number/index.html | 478 --- docs/field/input/password/index.html | 478 --- docs/field/input/range/index.html | 478 --- docs/field/input/text/index.html | 474 --- docs/field/text/index.html | 311 -- docs/fields/button/index.html | 166 +- docs/fields/date/date/index.html | 464 --- docs/fields/date/datetime/index.html | 468 --- docs/fields/date/index.html | 455 --- docs/fields/date/index.xml | 57 - docs/fields/date/time/index.html | 464 --- docs/fields/index.html | 287 +- docs/fields/index.xml | 246 +- docs/fields/input/date/index.html | 179 +- docs/fields/input/datetime/index.html | 173 +- docs/fields/input/hidden/index.html | 185 +- docs/fields/input/index.html | 562 --- docs/fields/input/index.xml | 160 - docs/fields/input/mail/index.html | 173 +- docs/fields/input/number/index.html | 173 +- docs/fields/input/password/index.html | 173 +- docs/fields/input/range/index.html | 179 +- docs/fields/input/text/index.html | 391 +- docs/fields/input/time/index.html | 179 +- docs/fields/textarea/index.html | 168 +- docs/form/bind/index.html | 651 --- docs/form/index.xml | 113 - docs/form/mount/index.html | 605 --- docs/index.html | 158 +- docs/index.xml | 187 +- docs/installation/index.html | 196 +- docs/introduction/index.html | 368 -- docs/rendering/index.html | 158 +- docs/rendering/theming/index.html | 158 +- docs/{form => workflow}/index.html | 315 +- docs/workflow/index.xml | 18 + en.search-data.json | 2 +- en.search.js | 437 -- ...86d6474e66bd5acdbbe01e8bac129f5ef345a88.js | 8 - index.html | 80 +- index.xml | 17 - js/main.js | 320 -- sitemap.xml | 10 +- tags/index.html | 80 +- 62 files changed, 1859 insertions(+), 18398 deletions(-) rename css/custom.css => .hugo_build.lock (100%) delete mode 100644 css/compiled/main.css delete mode 100644 docs/01-introduction/index.html delete mode 100644 docs/02-installation/index.html delete mode 100644 docs/button/index.html delete mode 100644 docs/field/date/date/index.html delete mode 100644 docs/field/date/datetime/index.html delete mode 100644 docs/field/date/index.html delete mode 100644 docs/field/date/index.xml delete mode 100644 docs/field/date/time/index.html delete mode 100644 docs/field/index.html delete mode 100644 docs/field/index.xml delete mode 100644 docs/field/input/hidden/index.html delete mode 100644 docs/field/input/index.html delete mode 100644 docs/field/input/index.xml delete mode 100644 docs/field/input/mail/index.html delete mode 100644 docs/field/input/number/index.html delete mode 100644 docs/field/input/password/index.html delete mode 100644 docs/field/input/range/index.html delete mode 100644 docs/field/input/text/index.html delete mode 100644 docs/field/text/index.html delete mode 100644 docs/fields/date/date/index.html delete mode 100644 docs/fields/date/datetime/index.html delete mode 100644 docs/fields/date/index.html delete mode 100644 docs/fields/date/index.xml delete mode 100644 docs/fields/date/time/index.html delete mode 100644 docs/fields/input/index.html delete mode 100644 docs/fields/input/index.xml delete mode 100644 docs/form/bind/index.html delete mode 100644 docs/form/index.xml delete mode 100644 docs/form/mount/index.html delete mode 100644 docs/introduction/index.html rename docs/{form => workflow}/index.html (76%) create mode 100644 docs/workflow/index.xml delete mode 100644 en.search.js delete mode 100644 en.search.min.7918d81999396a56d9ff1b7a986d6474e66bd5acdbbe01e8bac129f5ef345a88.js delete mode 100644 index.xml delete mode 100644 js/main.js diff --git a/css/custom.css b/.hugo_build.lock similarity index 100% rename from css/custom.css rename to .hugo_build.lock diff --git a/categories/index.html b/categories/index.html index c35083a..082cdbc 100644 --- a/categories/index.html +++ b/categories/index.html @@ -60,7 +60,7 @@ -
      +
      @@ -542,6 +482,10 @@
      Hidden - - - - - - - - - - - -Date – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      -
      - - - - - - -
      - -
      - - - - - - diff --git a/docs/fields/date/datetime/index.html b/docs/fields/date/datetime/index.html deleted file mode 100644 index 67cdac7..0000000 --- a/docs/fields/date/datetime/index.html +++ /dev/null @@ -1,468 +0,0 @@ - - - - - - - - - - - - -Datetime – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      -
      - - - - - - -
      - -
      - - - - - - diff --git a/docs/fields/date/index.html b/docs/fields/date/index.html deleted file mode 100644 index 25b15c9..0000000 --- a/docs/fields/date/index.html +++ /dev/null @@ -1,455 +0,0 @@ - - - - - - - - - - - - -deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - -
      -
      - - - - - - -
      - -
      - - - - - - diff --git a/docs/fields/date/index.xml b/docs/fields/date/index.xml deleted file mode 100644 index 4895401..0000000 --- a/docs/fields/date/index.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - deblan/go-form – - http://localhost:1313/docs/fields/date/ - Recent content on deblan/go-form - Hugo -- gohugo.io - en-us - - - - - - - - - - - Date - http://localhost:1313/docs/fields/date/date/ - Mon, 01 Jan 0001 00:00:00 +0000 - - http://localhost:1313/docs/fields/date/date/ - - - - - - - - - Datetime - http://localhost:1313/docs/fields/date/datetime/ - Mon, 01 Jan 0001 00:00:00 +0000 - - http://localhost:1313/docs/fields/date/datetime/ - - - - - - - - - Time - http://localhost:1313/docs/fields/date/time/ - Mon, 01 Jan 0001 00:00:00 +0000 - - http://localhost:1313/docs/fields/date/time/ - - - - - - - - - diff --git a/docs/fields/date/time/index.html b/docs/fields/date/time/index.html deleted file mode 100644 index bd0c000..0000000 --- a/docs/fields/date/time/index.html +++ /dev/null @@ -1,464 +0,0 @@ - - - - - - - - - - - - -Time – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      -
      - - - - - - -
      - -
      - - - - - - diff --git a/docs/fields/index.html b/docs/fields/index.html index e677f08..dcc4a10 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -10,19 +10,23 @@ -Input – deblan/go-form - +Fields – deblan/go-form + - + - - + + + - - + + @@ -60,7 +64,7 @@ -
      +
    @@ -342,52 +388,48 @@
  • Form - -
    - -
  • + >Workflow +
  • Input + >Fields
  • Form - - -
  • Input - - -
  • Form - -
    - -
  • + >Workflow +
  • Input + >Fields
    - Input -
    -
    Time
    @@ -545,10 +482,10 @@
    + >Range diff --git a/docs/fields/textarea/index.html b/docs/fields/textarea/index.html index f32158b..e47e935 100644 --- a/docs/fields/textarea/index.html +++ b/docs/fields/textarea/index.html @@ -62,7 +62,7 @@ -
    +
  • Form - - -
  • Input + >Fields
  • Form - -
    - -
  • + >Workflow +
  • Input + >Fields
    Textarea
    @@ -545,7 +485,11 @@ href="/go-form/docs/fields/button/" title="Button" class="hx-flex hx-max-w-[50%] hx-items-center hx-gap-1 hx-py-4 hx-text-base hx-font-medium hx-text-gray-600 hx-transition-colors [word-break:break-word] hover:hx-text-primary-600 dark:hx-text-gray-300 md:hx-text-lg ltr:hx-pr-4 rtl:hx-pl-4" - >Button + >ButtonDate diff --git a/docs/form/bind/index.html b/docs/form/bind/index.html deleted file mode 100644 index 238536a..0000000 --- a/docs/form/bind/index.html +++ /dev/null @@ -1,651 +0,0 @@ - - - - - - - - - - - - -Bind – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - -
    -
    - -
    - -
    - Form -
    Bind
    -
    - -
    -

    Bind

    -
    - -
    import (
    -	"net/http"
    -
    -    "gitnet.fr/deblan/go-form/form"
    -)
    -
    -type Person struct {
    -    Name string
    -    Age  int
    -}
    -
    -func createForm() *form.Form {
    -    f := form.NewForm()
    -
    -    // do stuff
    -
    -    return f
    -}
    -
    -http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    -    alice := Person{
    -        Name: "Alice",
    -        Age:  42,
    -    }
    -
    -    myForm := createForm()
    -
    -    if r.Method == myForm.Method {
    -        myForm.HandleRequest(r)
    -
    -        if myForm.IsSubmitted() && myForm.IsValid() {
    -            myForm.Bind(&data)
    -        }
    -    }
    -})
    - -
    -
    - -
    -
    - - -
    -
    -
    - -
    - - - - - - diff --git a/docs/form/index.xml b/docs/form/index.xml deleted file mode 100644 index 66a0932..0000000 --- a/docs/form/index.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - deblan/go-form – Form - https://deblan.gitnet.page/go-form/docs/form/ - Recent content in Form on deblan/go-form - Hugo -- gohugo.io - en-us - - - - - - - - - - - Mount - https://deblan.gitnet.page/go-form/docs/form/mount/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/form/mount/ - - - - <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Person</span> <span class="kd">struct</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Age</span> <span class="kt">int</span> -</span></span><span class="line"><span class="cl"><span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="nx">alice</span> <span class="o">:=</span> <span class="nx">Person</span><span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Age</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span> -</span></span><span class="line"><span class="cl"><span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="c1">// Assuming 2 fields named &#34;Name&#34; and &#34;Age&#34; exist</span> -</span></span><span class="line"><span class="cl"><span class="nx">myForm</span><span class="p">.</span><span class="nf">Mount</span><span class="p">(</span><span class="nx">alice</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> - - - - - - Bind - https://deblan.gitnet.page/go-form/docs/form/bind/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/form/bind/ - - - - <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> -</span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="s">&#34;gitnet.fr/deblan/go-form/form&#34;</span> -</span></span><span class="line"><span class="cl"><span class="p">)</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Person</span> <span class="kd">struct</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Age</span> <span class="kt">int</span> -</span></span><span class="line"><span class="cl"><span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">createForm</span><span class="p">()</span> <span class="o">*</span><span class="nx">form</span><span class="p">.</span><span class="nx">Form</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">f</span> <span class="o">:=</span> <span class="nx">form</span><span class="p">.</span><span class="nf">NewForm</span><span class="p">()</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="c1">// do stuff</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">f</span> -</span></span><span class="line"><span class="cl"><span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">alice</span> <span class="o">:=</span> <span class="nx">Person</span><span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Age</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span> -</span></span><span class="line"><span class="cl"> <span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span> <span class="o">:=</span> <span class="nf">createForm</span><span class="p">()</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Method</span> <span class="o">==</span> <span class="nx">myForm</span><span class="p">.</span><span class="nx">Method</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span><span class="p">.</span><span class="nf">HandleRequest</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">myForm</span><span class="p">.</span><span class="nf">IsSubmitted</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="nx">myForm</span><span class="p">.</span><span class="nf">IsValid</span><span class="p">()</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span><span class="p">.</span><span class="nf">Bind</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">data</span><span class="p">)</span> -</span></span><span class="line"><span class="cl"> <span class="p">}</span> -</span></span><span class="line"><span class="cl"> <span class="p">}</span> -</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> - - - - - - diff --git a/docs/form/mount/index.html b/docs/form/mount/index.html deleted file mode 100644 index 9b3321f..0000000 --- a/docs/form/mount/index.html +++ /dev/null @@ -1,605 +0,0 @@ - - - - - - - - - - - - -Mount – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - -
    -
    - -
    - -
    - Form -
    Mount
    -
    - -
    -

    Mount

    -
    - -
    type Person struct {
    -    Name string
    -    Age  int
    -}
    -
    -alice := Person{
    -    Name: "Alice",
    -    Age:  42,
    -}
    -
    -// Assuming 2 fields named "Name" and "Age" exist
    -myForm.Mount(alice)
    - -
    -
    - -
    -
    - - -
    -
    -
    - -
    - - - - - - diff --git a/docs/index.html b/docs/index.html index e1dc575..72f386c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -67,7 +67,7 @@ A form builder based on fields declarations and independent of structs Validatio -
    +
  • Form - - - -
  • Input + >Fields
    @@ -350,52 +320,48 @@ A form builder based on fields declarations and independent of structs Validatio
  • Form - -
    - -
  • + >Workflow +
  • Input + >Fields
    diff --git a/docs/index.xml b/docs/index.xml index d85cb08..cee4e2a 100644 --- a/docs/index.xml +++ b/docs/index.xml @@ -15,28 +15,33 @@ - Mount - https://deblan.gitnet.page/go-form/docs/form/mount/ + Text + https://deblan.gitnet.page/go-form/docs/fields/input/text/ Mon, 01 Jan 0001 00:00:00 +0000 - https://deblan.gitnet.page/go-form/docs/form/mount/ + https://deblan.gitnet.page/go-form/docs/fields/input/text/ - <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> + <h2>Basic example<span class="hx-absolute -hx-mt-20" id="basic-example"></span> + <a href="#basic-example" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-scrollbar hx-overflow-x-auto hx-overflow-y-hidden hx-overscroll-x-contain"> + <div class="hx-mt-4 hx-flex hx-w-max hx-min-w-full hx-border-b hx-border-gray-200 hx-pb-px dark:hx-border-neutral-800"><button + class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" + role="tab" + type="button" + aria-controls="tabs-panel-0" aria-selected="true" tabindex="0" data-state="selected">GO</button><button + class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" + role="tab" + type="button" + aria-controls="tabs-panel-1">Result</button></div> +</div> +<div> + <div + class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" + id="tabs-panel-0" + role="tabpanel" tabindex="0" data-state="selected" ><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Person</span> <span class="kd">struct</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Age</span> <span class="kt">int</span> -</span></span><span class="line"><span class="cl"><span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="nx">alice</span> <span class="o">:=</span> <span class="nx">Person</span><span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Age</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span> -</span></span><span class="line"><span class="cl"><span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="c1">// Assuming 2 fields named &#34;Name&#34; and &#34;Age&#34; exist</span> -</span></span><span class="line"><span class="cl"><span class="nx">myForm</span><span class="p">.</span><span class="nf">Mount</span><span class="p">(</span><span class="nx">alice</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="nx">form</span><span class="p">.</span><span class="nf">NewFieldText</span><span class="p">(</span><span class="s">&#34;Name&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> <button class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" title="Copy code" @@ -46,56 +51,17 @@ </button> </div> </div> +</div> + <div + class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" + id="tabs-panel-1" + role="tabpanel"><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - - - - - Bind - https://deblan.gitnet.page/go-form/docs/form/bind/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/form/bind/ - - - - <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> -</span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="s">&#34;gitnet.fr/deblan/go-form/form&#34;</span> -</span></span><span class="line"><span class="cl"><span class="p">)</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Person</span> <span class="kd">struct</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Age</span> <span class="kt">int</span> -</span></span><span class="line"><span class="cl"><span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">createForm</span><span class="p">()</span> <span class="o">*</span><span class="nx">form</span><span class="p">.</span><span class="nx">Form</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">f</span> <span class="o">:=</span> <span class="nx">form</span><span class="p">.</span><span class="nf">NewForm</span><span class="p">()</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="c1">// do stuff</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">f</span> -</span></span><span class="line"><span class="cl"><span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">alice</span> <span class="o">:=</span> <span class="nx">Person</span><span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> -</span></span><span class="line"><span class="cl"> <span class="nx">Age</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span> -</span></span><span class="line"><span class="cl"> <span class="p">}</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span> <span class="o">:=</span> <span class="nf">createForm</span><span class="p">()</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Method</span> <span class="o">==</span> <span class="nx">myForm</span><span class="p">.</span><span class="nx">Method</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span><span class="p">.</span><span class="nf">HandleRequest</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span> -</span></span><span class="line"><span class="cl"> -</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">myForm</span><span class="p">.</span><span class="nf">IsSubmitted</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="nx">myForm</span><span class="p">.</span><span class="nf">IsValid</span><span class="p">()</span> <span class="p">{</span> -</span></span><span class="line"><span class="cl"> <span class="nx">myForm</span><span class="p">.</span><span class="nf">Bind</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">data</span><span class="p">)</span> -</span></span><span class="line"><span class="cl"> <span class="p">}</span> -</span></span><span class="line"><span class="cl"> <span class="p">}</span> -</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;POST&#34;</span> <span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">div</span> <span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;form[Name]&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> <button class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" title="Copy code" @@ -104,6 +70,65 @@ <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> </button> </div> +</div> +</div> +</div> +<h2>Fully featured example<span class="hx-absolute -hx-mt-20" id="fully-featured-example"></span> + <a href="#fully-featured-example" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-scrollbar hx-overflow-x-auto hx-overflow-y-hidden hx-overscroll-x-contain"> + <div class="hx-mt-4 hx-flex hx-w-max hx-min-w-full hx-border-b hx-border-gray-200 hx-pb-px dark:hx-border-neutral-800"><button + class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" + role="tab" + type="button" + aria-controls="tabs-panel-0" aria-selected="true" tabindex="0" data-state="selected">GO</button><button + class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" + role="tab" + type="button" + aria-controls="tabs-panel-1">Result</button></div> +</div> +<div> + <div + class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" + id="tabs-panel-0" + role="tabpanel" tabindex="0" data-state="selected" ><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> + +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="nx">form</span><span class="p">.</span><span class="nf">NewFieldText</span><span class="p">(</span><span class="s">&#34;Name&#34;</span><span class="p">).</span> +</span></span><span class="line"><span class="cl"> <span class="nf">WithOptions</span><span class="p">(</span> +</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;label&#34;</span><span class="p">,</span> <span class="s">&#34;Name&#34;</span><span class="p">),</span> +</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;required&#34;</span><span class="p">,</span> <span class="kc">true</span><span class="p">),</span> +</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;attr&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;data-foo&#34;</span><span class="p">:</span> <span class="s">&#34;foo&#34;</span><span class="p">}),</span> +</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;row_attr&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;data-bar&#34;</span><span class="p">:</span> <span class="s">&#34;bar&#34;</span><span class="p">}),</span> +</span></span><span class="line"><span class="cl"> <span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> + <button + class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" + title="Copy code" + > + <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> + <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> + </button> +</div> +</div> +</div> + <div + class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" + id="tabs-panel-1" + role="tabpanel"><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> + +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;POST&#34;</span> <span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">data-bar</span><span class="o">=</span><span class="s">&#34;bar&#34;</span><span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="p">&gt;</span>Name<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="na">required</span><span class="o">=</span><span class="s">&#34;required&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;form[Name]&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">data-foo</span><span class="o">=</span><span class="s">&#34;foo&#34;</span><span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> +</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> + <button + class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" + title="Copy code" + > + <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> + <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> + </button> +</div> +</div> +</div> </div> @@ -131,7 +156,19 @@ - <h1>Installation</h1> + <div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> + +<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="k">go</span> <span class="nx">get</span> <span class="nx">gitnet</span><span class="p">.</span><span class="nx">fr</span><span class="o">/</span><span class="nx">deblan</span><span class="o">/</span><span class="k">go</span><span class="o">-</span><span class="nx">form</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> + <button + class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" + title="Copy code" + > + <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> + <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> + </button> +</div> +</div> + @@ -272,24 +309,6 @@ - - Text - https://deblan.gitnet.page/go-form/docs/fields/input/text/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/text/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - Time https://deblan.gitnet.page/go-form/docs/fields/input/time/ diff --git a/docs/installation/index.html b/docs/installation/index.html index 432c549..a73def4 100644 --- a/docs/installation/index.html +++ b/docs/installation/index.html @@ -11,21 +11,29 @@ Installation – deblan/go-form - + - + - - + + - + @@ -63,7 +71,7 @@ -
  • Form - - - -
  • Input + >Fields
  • Form - -
    - -
  • + >Workflow +
  • Input + >Fields
  • Form - - - -
  • Input + >Fields
  • Form - -
    - -
  • + >Workflow +
  • Input + >Fields
    diff --git a/docs/rendering/theming/index.html b/docs/rendering/theming/index.html index 4c41076..68b1cb6 100644 --- a/docs/rendering/theming/index.html +++ b/docs/rendering/theming/index.html @@ -62,7 +62,7 @@ -
    +
  • Form - - - -
  • Input + >Fields
  • Form - -
    - -
  • + >Workflow +
  • Input + >Fields
    diff --git a/docs/form/index.html b/docs/workflow/index.html similarity index 76% rename from docs/form/index.html rename to docs/workflow/index.html index ede746e..e8c6d2d 100644 --- a/docs/form/index.html +++ b/docs/workflow/index.html @@ -10,23 +10,20 @@ -Form – deblan/go-form - +Workflow – deblan/go-form + - + - + - - - + + + - - + + @@ -64,7 +61,7 @@ Fields Options Method Action Import import ( "net/http" "gitnet.fr/d -
    +
  • Form - + >Workflow - -
  • Input + >Fields
    @@ -359,52 +314,48 @@ Fields Options Method Action Import import ( "net/http" "gitnet.fr/d
  • Form - -
    - -
  • + >Workflow +
  • Input + >Fields
    @@ -535,15 +460,27 @@ Fields Options Method Action Import import ( "net/http" "gitnet.fr/d
  • Form - - - -
  • Input + >Fields
    diff --git a/index.xml b/index.xml deleted file mode 100644 index 27ad978..0000000 --- a/index.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - deblan/go-form – Welcome! - http://localhost:1313/ - Recent content in Welcome! on deblan/go-form - Hugo -- gohugo.io - en-us - - - - - - - - - - diff --git a/js/main.js b/js/main.js deleted file mode 100644 index 7d34dbb..0000000 --- a/js/main.js +++ /dev/null @@ -1,320 +0,0 @@ -// Light / Dark theme toggle -(function () { - const defaultTheme = 'system' - - const themeToggleButtons = document.querySelectorAll(".theme-toggle"); - - // Change the icons of the buttons based on previous settings or system theme - if ( - localStorage.getItem("color-theme") === "dark" || - (!("color-theme" in localStorage) && - ((window.matchMedia("(prefers-color-scheme: dark)").matches && defaultTheme === "system") || defaultTheme === "dark")) - ) { - themeToggleButtons.forEach((el) => el.dataset.theme = "dark"); - } else { - themeToggleButtons.forEach((el) => el.dataset.theme = "light"); - } - - // Add click event handler to the buttons - themeToggleButtons.forEach((el) => { - el.addEventListener("click", function () { - if (localStorage.getItem("color-theme")) { - if (localStorage.getItem("color-theme") === "light") { - setDarkTheme(); - localStorage.setItem("color-theme", "dark"); - } else { - setLightTheme(); - localStorage.setItem("color-theme", "light"); - } - } else { - if (document.documentElement.classList.contains("dark")) { - setLightTheme(); - localStorage.setItem("color-theme", "light"); - } else { - setDarkTheme(); - localStorage.setItem("color-theme", "dark"); - } - } - el.dataset.theme = document.documentElement.classList.contains("dark") ? "dark" : "light"; - }); - }); - - // Listen for system theme changes - window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => { - if (defaultTheme === "system" && !("color-theme" in localStorage)) { - e.matches ? setDarkTheme() : setLightTheme(); - themeToggleButtons.forEach((el) => - el.dataset.theme = document.documentElement.classList.contains("dark") ? "dark" : "light" - ); - } - }); -})(); - -; -// Hamburger menu for mobile navigation - -document.addEventListener('DOMContentLoaded', function () { - const menu = document.querySelector('.hamburger-menu'); - const overlay = document.querySelector('.mobile-menu-overlay'); - const sidebarContainer = document.querySelector('.sidebar-container'); - - // Initialize the overlay - const overlayClasses = ['hx-fixed', 'hx-inset-0', 'hx-z-10', 'hx-bg-black/80', 'dark:hx-bg-black/60']; - overlay.classList.add('hx-bg-transparent'); - overlay.classList.remove("hx-hidden", ...overlayClasses); - - function toggleMenu() { - // Toggle the hamburger menu - menu.querySelector('svg').classList.toggle('open'); - - // When the menu is open, we want to show the navigation sidebar - sidebarContainer.classList.toggle('max-md:[transform:translate3d(0,-100%,0)]'); - sidebarContainer.classList.toggle('max-md:[transform:translate3d(0,0,0)]'); - - // When the menu is open, we want to prevent the body from scrolling - document.body.classList.toggle('hx-overflow-hidden'); - document.body.classList.toggle('md:hx-overflow-auto'); - } - - function hideOverlay() { - // Hide the overlay - overlay.classList.remove(...overlayClasses); - overlay.classList.add('hx-bg-transparent'); - } - - menu.addEventListener('click', (e) => { - e.preventDefault(); - toggleMenu(); - - if (overlay.classList.contains('hx-bg-transparent')) { - // Show the overlay - overlay.classList.add(...overlayClasses); - overlay.classList.remove('hx-bg-transparent'); - } else { - // Hide the overlay - hideOverlay(); - } - }); - - overlay.addEventListener('click', (e) => { - e.preventDefault(); - toggleMenu(); - - // Hide the overlay - hideOverlay(); - }); - - // Select all anchor tags in the sidebar container - const sidebarLinks = sidebarContainer.querySelectorAll('a'); - - // Add click event listener to each anchor tag - sidebarLinks.forEach(link => { - link.addEventListener('click', (e) => { - // Check if the href attribute contains a hash symbol (links to a heading) - if (link.getAttribute('href') && link.getAttribute('href').startsWith('#')) { - // Only dismiss overlay on mobile view - if (window.innerWidth < 768) { - toggleMenu(); - hideOverlay(); - } - } - }); - }); -}); - -; -// Copy button for code blocks - -document.addEventListener('DOMContentLoaded', function () { - const getCopyIcon = () => { - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.innerHTML = ` - - `; - svg.setAttribute('fill', 'none'); - svg.setAttribute('viewBox', '0 0 24 24'); - svg.setAttribute('stroke', 'currentColor'); - svg.setAttribute('stroke-width', '2'); - return svg; - } - - const getSuccessIcon = () => { - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.innerHTML = ` - - `; - svg.setAttribute('fill', 'none'); - svg.setAttribute('viewBox', '0 0 24 24'); - svg.setAttribute('stroke', 'currentColor'); - svg.setAttribute('stroke-width', '2'); - return svg; - } - - document.querySelectorAll('.hextra-code-copy-btn').forEach(function (button) { - // Add copy and success icons - button.querySelector('.copy-icon')?.appendChild(getCopyIcon()); - button.querySelector('.success-icon')?.appendChild(getSuccessIcon()); - - // Add click event listener for copy button - button.addEventListener('click', function (e) { - e.preventDefault(); - // Get the code target - const target = button.parentElement.previousElementSibling; - let codeElement; - if (target.tagName === 'CODE') { - codeElement = target; - } else { - // Select the last code element in case line numbers are present - const codeElements = target.querySelectorAll('code'); - codeElement = codeElements[codeElements.length - 1]; - } - if (codeElement) { - let code = codeElement.innerText; - // Replace double newlines with single newlines in the innerText - // as each line inside has trailing newline '\n' - if ("lang" in codeElement.dataset) { - code = code.replace(/\n\n/g, '\n'); - } - navigator.clipboard.writeText(code).then(function () { - button.classList.add('copied'); - setTimeout(function () { - button.classList.remove('copied'); - }, 1000); - }).catch(function (err) { - console.error('Failed to copy text: ', err); - }); - } else { - console.error('Target element not found'); - } - }); - }); -}); - -; -document.querySelectorAll('.hextra-tabs-toggle').forEach(function (button) { - button.addEventListener('click', function (e) { - // set parent tabs to unselected - const tabs = Array.from(e.target.parentElement.querySelectorAll('.hextra-tabs-toggle')); - tabs.map(tab => tab.dataset.state = ''); - - // set current tab to selected - e.target.dataset.state = 'selected'; - - // set all panels to unselected - const panelsContainer = e.target.parentElement.parentElement.nextElementSibling; - Array.from(panelsContainer.children).forEach(function (panel) { - panel.dataset.state = ''; - }); - - const panelId = e.target.getAttribute('aria-controls'); - const panel = panelsContainer.querySelector(`#${panelId}`); - panel.dataset.state = 'selected'; - }); -}); - -; -(function () { - const languageSwitchers = document.querySelectorAll('.language-switcher'); - languageSwitchers.forEach((switcher) => { - switcher.addEventListener('click', (e) => { - e.preventDefault(); - switcher.dataset.state = switcher.dataset.state === 'open' ? 'closed' : 'open'; - const optionsElement = switcher.nextElementSibling; - optionsElement.classList.toggle('hx-hidden'); - - // Calculate position of language options element - const switcherRect = switcher.getBoundingClientRect(); - const translateY = switcherRect.top - window.innerHeight - 15; - optionsElement.style.transform = `translate3d(${switcherRect.left}px, ${translateY}px, 0)`; - optionsElement.style.minWidth = `${Math.max(switcherRect.width, 50)}px`; - }); - }); - - // Dismiss language switcher when clicking outside - document.addEventListener('click', (e) => { - if (e.target.closest('.language-switcher') === null) { - languageSwitchers.forEach((switcher) => { - switcher.dataset.state = 'closed'; - const optionsElement = switcher.nextElementSibling; - optionsElement.classList.add('hx-hidden'); - }); - } - }); -})(); - -; -// Script for filetree shortcode collapsing/expanding folders used in the theme -// ====================================================================== -document.addEventListener("DOMContentLoaded", function () { - const folders = document.querySelectorAll(".hextra-filetree-folder"); - folders.forEach(function (folder) { - folder.addEventListener("click", function () { - Array.from(folder.children).forEach(function (el) { - el.dataset.state = el.dataset.state === "open" ? "closed" : "open"; - }); - folder.nextElementSibling.dataset.state = folder.nextElementSibling.dataset.state === "open" ? "closed" : "open"; - }); - }); -}); - -; -document.addEventListener("DOMContentLoaded", function () { - scrollToActiveItem(); - enableCollapsibles(); -}); - -function enableCollapsibles() { - const buttons = document.querySelectorAll(".hextra-sidebar-collapsible-button"); - buttons.forEach(function (button) { - button.addEventListener("click", function (e) { - e.preventDefault(); - const list = button.parentElement.parentElement; - if (list) { - list.classList.toggle("open") - } - }); - }); -} - -function scrollToActiveItem() { - const sidebarScrollbar = document.querySelector("aside.sidebar-container > .hextra-scrollbar"); - const activeItems = document.querySelectorAll(".sidebar-active-item"); - const visibleActiveItem = Array.from(activeItems).find(function (activeItem) { - return activeItem.getBoundingClientRect().height > 0; - }); - - if (!visibleActiveItem) { - return; - } - - const yOffset = visibleActiveItem.clientHeight; - const yDistance = visibleActiveItem.getBoundingClientRect().top - sidebarScrollbar.getBoundingClientRect().top; - sidebarScrollbar.scrollTo({ - behavior: "instant", - top: yDistance - yOffset - }); -} - -; -// Back to top button - -document.addEventListener("DOMContentLoaded", function () { - const backToTop = document.querySelector("#backToTop"); - if (backToTop) { - document.addEventListener("scroll", (e) => { - if (window.scrollY > 300) { - backToTop.classList.remove("hx-opacity-0"); - } else { - backToTop.classList.add("hx-opacity-0"); - } - }); - } -}); - -function scrollUp() { - window.scroll({ - top: 0, - left: 0, - behavior: "smooth", - }); -} diff --git a/sitemap.xml b/sitemap.xml index 58b15cb..29b158b 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,19 +2,15 @@ - https://deblan.gitnet.page/go-form/docs/form/mount/ - - https://deblan.gitnet.page/go-form/docs/form/bind/ + https://deblan.gitnet.page/go-form/docs/fields/input/text/ https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ https://deblan.gitnet.page/go-form/docs/installation/ - https://deblan.gitnet.page/go-form/docs/form/ + https://deblan.gitnet.page/go-form/docs/workflow/ https://deblan.gitnet.page/go-form/docs/fields/ - - https://deblan.gitnet.page/go-form/docs/fields/input/ https://deblan.gitnet.page/go-form/docs/constraints/ @@ -43,8 +39,6 @@ https://deblan.gitnet.page/go-form/docs/fields/input/range/ https://deblan.gitnet.page/go-form/tags/ - - https://deblan.gitnet.page/go-form/docs/fields/input/text/ https://deblan.gitnet.page/go-form/docs/fields/input/time/ diff --git a/tags/index.html b/tags/index.html index e60460f..5af01b3 100644 --- a/tags/index.html +++ b/tags/index.html @@ -60,7 +60,7 @@ -
    +
  • Form - - - -
  • Input + >Fields
    From 8a083f120412cedc25ebbf331583ca18e4484847 Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 22 Jul 2025 14:19:03 +0000 Subject: [PATCH 030/117] Build doc --- categories/index.html | 24 +- docs/constraints/index.html | 47 +- docs/fields/button/index.html | 45 +- docs/fields/index.html | 45 +- docs/fields/input/date/index.html | 45 +- docs/fields/input/datetime/index.html | 45 +- docs/fields/input/hidden/index.html | 45 +- docs/fields/input/mail/index.html | 45 +- docs/fields/input/number/index.html | 45 +- docs/fields/input/password/index.html | 45 +- docs/fields/input/range/index.html | 45 +- docs/fields/input/text/index.html | 45 +- docs/fields/input/time/index.html | 45 +- docs/fields/textarea/index.html | 45 +- docs/form/index.html | 1032 +++++++++++++++++++++++++ docs/form/index.xml | 18 + docs/index.html | 77 +- docs/installation/index.html | 45 +- docs/rendering/index.html | 45 +- docs/rendering/theming/index.html | 45 +- docs/workflow/index.html | 45 +- en.search-data.json | 2 +- index.html | 24 +- sitemap.xml | 4 +- tags/index.html | 24 +- 25 files changed, 1647 insertions(+), 325 deletions(-) create mode 100644 docs/form/index.html create mode 100644 docs/form/index.xml diff --git a/categories/index.html b/categories/index.html index 082cdbc..2943a52 100644 --- a/categories/index.html +++ b/categories/index.html @@ -160,6 +160,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering diff --git a/docs/constraints/index.html b/docs/constraints/index.html index 1a81679..4e22565 100644 --- a/docs/constraints/index.html +++ b/docs/constraints/index.html @@ -162,6 +162,23 @@ >Workflow +
  • Form + + +
  • Constraints + + +
  • -
  • Constraints - - -
  • Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -310,6 +318,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/index.html b/docs/fields/index.html index dcc4a10..6ce26a5 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -164,6 +164,22 @@ import ( "bytes" "fmt" "html/template" "gitnet.fr/de href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -320,6 +328,20 @@ import ( "bytes" "fmt" "html/template" "gitnet.fr/de href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/date/index.html b/docs/fields/input/date/index.html index 4b4faf9..445f1c7 100644 --- a/docs/fields/input/date/index.html +++ b/docs/fields/input/date/index.html @@ -162,6 +162,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -310,6 +318,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/datetime/index.html b/docs/fields/input/datetime/index.html index 2bf9399..1347ad0 100644 --- a/docs/fields/input/datetime/index.html +++ b/docs/fields/input/datetime/index.html @@ -162,6 +162,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -310,6 +318,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/hidden/index.html b/docs/fields/input/hidden/index.html index e0a14bd..f28d15e 100644 --- a/docs/fields/input/hidden/index.html +++ b/docs/fields/input/hidden/index.html @@ -162,6 +162,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -310,6 +318,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/mail/index.html b/docs/fields/input/mail/index.html index 47045dd..56a40fe 100644 --- a/docs/fields/input/mail/index.html +++ b/docs/fields/input/mail/index.html @@ -167,6 +167,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -315,6 +323,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/number/index.html b/docs/fields/input/number/index.html index e86ff72..f178fbb 100644 --- a/docs/fields/input/number/index.html +++ b/docs/fields/input/number/index.html @@ -167,6 +167,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -315,6 +323,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/password/index.html b/docs/fields/input/password/index.html index 6edbafc..c4ff620 100644 --- a/docs/fields/input/password/index.html +++ b/docs/fields/input/password/index.html @@ -167,6 +167,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -315,6 +323,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/range/index.html b/docs/fields/input/range/index.html index 8883770..cec9346 100644 --- a/docs/fields/input/range/index.html +++ b/docs/fields/input/range/index.html @@ -167,6 +167,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -315,6 +323,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/text/index.html b/docs/fields/input/text/index.html index 250fff2..8222a90 100644 --- a/docs/fields/input/text/index.html +++ b/docs/fields/input/text/index.html @@ -229,6 +229,22 @@ form.NewFieldText("Name"). href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -391,6 +399,20 @@ form.NewFieldText("Name"). href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/input/time/index.html b/docs/fields/input/time/index.html index fe12846..85ac96e 100644 --- a/docs/fields/input/time/index.html +++ b/docs/fields/input/time/index.html @@ -162,6 +162,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -310,6 +318,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/fields/textarea/index.html b/docs/fields/textarea/index.html index e47e935..f3583a0 100644 --- a/docs/fields/textarea/index.html +++ b/docs/fields/textarea/index.html @@ -162,6 +162,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -310,6 +318,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/form/index.html b/docs/form/index.html new file mode 100644 index 0000000..e9b0711 --- /dev/null +++ b/docs/form/index.html @@ -0,0 +1,1032 @@ + + + + + + + + + + + + +Form – deblan/go-form + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + +
    +
    + +
    +
    Form
    +
    + +
    +

    Form

    +

    Example +

    Prerequisites +

    + +
    import (
    +	"gitnet.fr/deblan/go-form/form"
    +	"gitnet.fr/deblan/go-form/validation"
    +)
    +
    +type Person struct {
    +    Name string
    +    Age  int
    +}
    + +
    +
    +

    Creating a form +

    + +
    myForm := form.NewForm(
    +    form.NewFieldText("Name").
    +        WithOptions(
    +            form.NewOption("label", "Person"),
    +            form.NewOption("required", true),
    +        ).
    +        WithConstraints(
    +            validation.NewNotBlank(),
    +        ),
    +    form.NewFieldNumber("Age").
    +        WithOptions(
    +            form.NewOption("label", "Age"),
    +            form.NewOption("required", true),
    +        ).
    +        WithConstraints(
    +            validation.NewNotBlank(),
    +            validation.New(),
    +            validation.NewRange().WithMin(18),
    +        ),
    +).End()
    + +
    +
    +

    Validating a struct +

    + +
    data := Person{}
    +
    +myForm.Mount(data)
    +myForm.IsValid() // false
    +
    +data = Person{
    +    Name: "Alice",
    +    Age:  42,
    +}
    +
    +myForm.Mount(data)
    +myForm.IsValid() // true
    + +
    +
    +

    Validating a request +

    + +
    import (
    +    "net/http"
    +)
    +
    +myForm.WithMethod(http.MethodPost)
    +
    +// req *http.Request
    +if req.Method == myForm.Method {
    +    myForm.HandleRequest(req)
    +
    +    if myForm.IsSubmitted() && myForm.IsValid() {
    +        myForm.Bind(&data)
    +    }
    +}
    + +
    +
    +

    Struct +

    + +
    type Form struct {
    +	Fields       []*Field
    +	GlobalFields []*Field
    +	Errors       []validation.Error
    +	Method       string
    +	Action       string
    +	Name         string
    +	Options      []*Option
    +	RequestData  *url.Values
    +}
    + +
    +
    +

    Methods +

    NewForm +

    + +
    func NewForm(fields ...*Field) *Form
    + +
    +
    +

    Generates a new form with default properties

    +

    Add +

    + +
    func (f *Form) Add(fields ...*Field)
    + +
    +
    +

    Appends children

    +

    AddGlobalField +

    + +
    func (f *Form) AddGlobalField(field *Field)
    + +
    +
    +

    Configures its children deeply

    +

    Bind +

    + +
    func (f *Form) Bind(data any) error
    + +
    +
    +

    Copies datas from the form to a struct

    +

    End +

    + +
    func (f *Form) End() *Form
    + +
    +
    +

    Configures its children deeply This function must be called after adding all

    +

    fields

    +

    GetField +

    + +
    func (f *Form) GetField(name string) *Field
    + +
    +
    +

    Returns a child using its name

    +

    GetOption +

    + +
    func (f *Form) GetOption(name string) *Option
    + +
    +
    +

    Returns an option using its name

    +

    HandleRequest +

    + +
    func (f *Form) HandleRequest(req *http.Request)
    + +
    +
    +

    Processes a request

    +

    HasField +

    + +
    func (f *Form) HasField(name string) bool
    + +
    +
    +

    Checks if the form contains a child using its name

    +

    HasOption +

    + +
    func (f *Form) HasOption(name string) bool
    + +
    +
    +

    Checks if the form contains an option using its name

    +

    IsSubmitted +

    + +
    func (f *Form) IsSubmitted() bool
    + +
    +
    +

    Checks if the form is submitted

    +

    IsValid +

    + +
    func (f *Form) IsValid() bool
    + +
    +
    +

    Checks the a form is valid

    +

    Mount +

    + +
    func (f *Form) Mount(data any) error
    + +
    +
    +

    Copies datas from a struct to the form

    +

    ResetErrors +

    + +
    func (f *Form) ResetErrors() *Form
    + +
    +
    +

    Resets the form errors

    +

    WithAction +

    + +
    func (f *Form) WithAction(v string) *Form
    + +
    +
    +

    Sets the action of the form (eg: “/”)

    +

    WithMethod +

    + +
    func (f *Form) WithMethod(v string) *Form
    + +
    +
    +

    Sets the method of the format (http.MethodPost, http.MethodGet, …)

    +

    WithName +

    + +
    func (f *Form) WithName(v string) *Form
    + +
    +
    +

    Sets the name of the form (used to compute name of fields)

    +

    WithOptions +

    + +
    func (f *Form) WithOptions(options ...*Option) *Form
    + +
    +
    +

    Appends options to the form

    +

    Options +

    + + + + + + + + + + + + + + + + +
    NameTypeDescriptionInfo
    attrmap[string]stringList of extra attributes
    + +
    +
    + + +
    +
    +
    + +
    + + + + + + diff --git a/docs/form/index.xml b/docs/form/index.xml new file mode 100644 index 0000000..4e02c53 --- /dev/null +++ b/docs/form/index.xml @@ -0,0 +1,18 @@ + + + deblan/go-form – Form + https://deblan.gitnet.page/go-form/docs/form/ + Recent content in Form on deblan/go-form + Hugo -- gohugo.io + en-us + + + + + + + + + + + diff --git a/docs/index.html b/docs/index.html index 72f386c..f5e6d08 100644 --- a/docs/index.html +++ b/docs/index.html @@ -24,7 +24,7 @@ A form builder based on fields declarations and independent of structs Validatio - + - -
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering @@ -319,6 +327,20 @@ go get gitnet.fr/deblan/go-form href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/docs/rendering/index.html b/docs/rendering/index.html index f32b02b..b1cba71 100644 --- a/docs/rendering/index.html +++ b/docs/rendering/index.html @@ -160,6 +160,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • -
  • Constraints - -
  • Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • Workflow + + +
  • Form + + +
  • Constraints
  • -
  • Constraints - -
  • Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • +
  • Form + + +
  • Constraints + +
  • Constraints - - -
  • Rendering @@ -317,6 +325,20 @@ href="/go-form/docs/workflow/" >Workflow +
  • +
  • Form +
  • +
  • Constraints
  • Constraints -
  • -
  • Rendering diff --git a/en.search-data.json b/en.search-data.json index fffb466..d2d8470 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes ","questions-or-feedback#Questions or Feedback?":" fmt.Sprintf(\"foo\") "},"title":"Documentation"},"/go-form/docs/fields/":{"data":{"":"The documentation of field is realised with this program.\nimport ( \"bytes\" \"fmt\" \"html/template\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { myForm := form.NewForm( // the documented field here // form.NewField... ). WithName(\"form\"). End() render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse(`{{- form .Form -}}`) var buffer bytes.Buffer tpl.Execute(\u0026buffer, map[string]any{ \"Form\": myForm, }) fmt.Print(buffer.String()) } ","common-options#Common options":" Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field "},"title":"Fields"},"/go-form/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/go-form/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/go-form/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/go-form/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/go-form/docs/fields/input/text/":{"data":{"":"","basic-example#Basic example":" GOResult form.NewFieldText(\"Name\") \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv \u003e \u003cinput id=\"form-name\" name=\"form[Name]\" value=\"\" type=\"text\" \u003e \u003c/div\u003e \u003c/form\u003e ","fully-featured-example#Fully featured example":" GOResult form.NewFieldText(\"Name\"). WithOptions( form.NewOption(\"label\", \"Name\"), form.NewOption(\"required\", true), form.NewOption(\"attr\", map[string]string{\"data-foo\": \"foo\"}), form.NewOption(\"row_attr\", map[string]string{\"data-bar\": \"bar\"}), ) \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv data-bar=\"bar\"\u003e \u003clabel for=\"form-name\" \u003eName\u003c/label\u003e \u003cinput id=\"form-name\" required=\"required\" name=\"form[Name]\" value=\"\" type=\"text\" data-foo=\"foo\"\u003e \u003c/div\u003e \u003c/form\u003e "},"title":"Text"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/fields/":{"data":{"":"The documentation of field is realised with this program.\nimport ( \"bytes\" \"fmt\" \"html/template\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { myForm := form.NewForm( // the documented field here // form.NewField... ). WithName(\"form\"). End() render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse(`{{- form .Form -}}`) var buffer bytes.Buffer tpl.Execute(\u0026buffer, map[string]any{ \"Form\": myForm, }) fmt.Print(buffer.String()) } ","common-options#Common options":" Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field "},"title":"Fields"},"/go-form/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/go-form/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/go-form/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/go-form/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/go-form/docs/fields/input/text/":{"data":{"":"","basic-example#Basic example":" GOResult form.NewFieldText(\"Name\") \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv \u003e \u003cinput id=\"form-name\" name=\"form[Name]\" value=\"\" type=\"text\" \u003e \u003c/div\u003e \u003c/form\u003e ","fully-featured-example#Fully featured example":" GOResult form.NewFieldText(\"Name\"). WithOptions( form.NewOption(\"label\", \"Name\"), form.NewOption(\"required\", true), form.NewOption(\"attr\", map[string]string{\"data-foo\": \"foo\"}), form.NewOption(\"row_attr\", map[string]string{\"data-bar\": \"bar\"}), ) \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv data-bar=\"bar\"\u003e \u003clabel for=\"form-name\" \u003eName\u003c/label\u003e \u003cinput id=\"form-name\" required=\"required\" name=\"form[Name]\" value=\"\" type=\"text\" data-foo=\"foo\"\u003e \u003c/div\u003e \u003c/form\u003e "},"title":"Text"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithOptions( form.NewOption(\"label\", \"Person\"), form.NewOption(\"required\", true), ). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithOptions( form.NewOption(\"label\", \"Age\"), form.NewOption(\"required\", true), ). WithConstraints( validation.NewNotBlank(), validation.New(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description Info attr map[string]string List of extra attributes ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file diff --git a/index.html b/index.html index 555dd4f..5d7f89e 100644 --- a/index.html +++ b/index.html @@ -162,6 +162,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering diff --git a/sitemap.xml b/sitemap.xml index 29b158b..dcab1ff 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -10,9 +10,11 @@ https://deblan.gitnet.page/go-form/docs/workflow/ - https://deblan.gitnet.page/go-form/docs/fields/ + https://deblan.gitnet.page/go-form/docs/form/ https://deblan.gitnet.page/go-form/docs/constraints/ + + https://deblan.gitnet.page/go-form/docs/fields/ https://deblan.gitnet.page/go-form/docs/fields/button/ diff --git a/tags/index.html b/tags/index.html index 5af01b3..76292e7 100644 --- a/tags/index.html +++ b/tags/index.html @@ -160,6 +160,22 @@ href="/go-form/docs/workflow/" >Workflow + + +
  • Form + + +
  • Constraints
  • Constraints - - -
  • Rendering From 5f41360b1fc292d2ae210f24b875cf0c750fd292 Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 22 Jul 2025 14:29:48 +0000 Subject: [PATCH 031/117] Build doc --- docs/form/index.html | 17 ++++------------- en.search-data.json | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/docs/form/index.html b/docs/form/index.html index e9b0711..4e3210f 100644 --- a/docs/form/index.html +++ b/docs/form/index.html @@ -11,7 +11,7 @@ Form – deblan/go-form - + @@ -19,11 +19,11 @@ - - + + - + @@ -640,21 +640,12 @@
    myForm := form.NewForm(
         form.NewFieldText("Name").
    -        WithOptions(
    -            form.NewOption("label", "Person"),
    -            form.NewOption("required", true),
    -        ).
             WithConstraints(
                 validation.NewNotBlank(),
             ),
         form.NewFieldNumber("Age").
    -        WithOptions(
    -            form.NewOption("label", "Age"),
    -            form.NewOption("required", true),
    -        ).
             WithConstraints(
                 validation.NewNotBlank(),
    -            validation.New(),
                 validation.NewRange().WithMin(18),
             ),
     ).End()
    diff --git a/en.search-data.json b/en.search-data.json index d2d8470..66e373a 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/fields/":{"data":{"":"The documentation of field is realised with this program.\nimport ( \"bytes\" \"fmt\" \"html/template\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { myForm := form.NewForm( // the documented field here // form.NewField... ). WithName(\"form\"). End() render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse(`{{- form .Form -}}`) var buffer bytes.Buffer tpl.Execute(\u0026buffer, map[string]any{ \"Form\": myForm, }) fmt.Print(buffer.String()) } ","common-options#Common options":" Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field "},"title":"Fields"},"/go-form/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/go-form/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/go-form/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/go-form/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/go-form/docs/fields/input/text/":{"data":{"":"","basic-example#Basic example":" GOResult form.NewFieldText(\"Name\") \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv \u003e \u003cinput id=\"form-name\" name=\"form[Name]\" value=\"\" type=\"text\" \u003e \u003c/div\u003e \u003c/form\u003e ","fully-featured-example#Fully featured example":" GOResult form.NewFieldText(\"Name\"). WithOptions( form.NewOption(\"label\", \"Name\"), form.NewOption(\"required\", true), form.NewOption(\"attr\", map[string]string{\"data-foo\": \"foo\"}), form.NewOption(\"row_attr\", map[string]string{\"data-bar\": \"bar\"}), ) \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv data-bar=\"bar\"\u003e \u003clabel for=\"form-name\" \u003eName\u003c/label\u003e \u003cinput id=\"form-name\" required=\"required\" name=\"form[Name]\" value=\"\" type=\"text\" data-foo=\"foo\"\u003e \u003c/div\u003e \u003c/form\u003e "},"title":"Text"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithOptions( form.NewOption(\"label\", \"Person\"), form.NewOption(\"required\", true), ). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithOptions( form.NewOption(\"label\", \"Age\"), form.NewOption(\"required\", true), ). WithConstraints( validation.NewNotBlank(), validation.New(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description Info attr map[string]string List of extra attributes ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/fields/":{"data":{"":"The documentation of field is realised with this program.\nimport ( \"bytes\" \"fmt\" \"html/template\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { myForm := form.NewForm( // the documented field here // form.NewField... ). WithName(\"form\"). End() render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse(`{{- form .Form -}}`) var buffer bytes.Buffer tpl.Execute(\u0026buffer, map[string]any{ \"Form\": myForm, }) fmt.Print(buffer.String()) } ","common-options#Common options":" Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field "},"title":"Fields"},"/go-form/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/go-form/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/go-form/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/go-form/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/go-form/docs/fields/input/text/":{"data":{"":"","basic-example#Basic example":" GOResult form.NewFieldText(\"Name\") \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv \u003e \u003cinput id=\"form-name\" name=\"form[Name]\" value=\"\" type=\"text\" \u003e \u003c/div\u003e \u003c/form\u003e ","fully-featured-example#Fully featured example":" GOResult form.NewFieldText(\"Name\"). WithOptions( form.NewOption(\"label\", \"Name\"), form.NewOption(\"required\", true), form.NewOption(\"attr\", map[string]string{\"data-foo\": \"foo\"}), form.NewOption(\"row_attr\", map[string]string{\"data-bar\": \"bar\"}), ) \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv data-bar=\"bar\"\u003e \u003clabel for=\"form-name\" \u003eName\u003c/label\u003e \u003cinput id=\"form-name\" required=\"required\" name=\"form[Name]\" value=\"\" type=\"text\" data-foo=\"foo\"\u003e \u003c/div\u003e \u003c/form\u003e "},"title":"Text"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description Info attr map[string]string List of extra attributes ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From cc76e3e580cf947fa53e5d10489e1513802357a5 Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 22 Jul 2025 14:32:39 +0000 Subject: [PATCH 032/117] Build doc --- docs/form/index.html | 9 ++++++--- en.search-data.json | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/form/index.html b/docs/form/index.html index 4e3210f..5d01da3 100644 --- a/docs/form/index.html +++ b/docs/form/index.html @@ -20,7 +20,7 @@ - + @@ -991,7 +991,6 @@ Name Type Description - Info @@ -999,7 +998,11 @@ attr map[string]string List of extra attributes - + + + help + string + Helper diff --git a/en.search-data.json b/en.search-data.json index 66e373a..884a443 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/fields/":{"data":{"":"The documentation of field is realised with this program.\nimport ( \"bytes\" \"fmt\" \"html/template\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { myForm := form.NewForm( // the documented field here // form.NewField... ). WithName(\"form\"). End() render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse(`{{- form .Form -}}`) var buffer bytes.Buffer tpl.Execute(\u0026buffer, map[string]any{ \"Form\": myForm, }) fmt.Print(buffer.String()) } ","common-options#Common options":" Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field "},"title":"Fields"},"/go-form/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/go-form/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/go-form/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/go-form/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/go-form/docs/fields/input/text/":{"data":{"":"","basic-example#Basic example":" GOResult form.NewFieldText(\"Name\") \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv \u003e \u003cinput id=\"form-name\" name=\"form[Name]\" value=\"\" type=\"text\" \u003e \u003c/div\u003e \u003c/form\u003e ","fully-featured-example#Fully featured example":" GOResult form.NewFieldText(\"Name\"). WithOptions( form.NewOption(\"label\", \"Name\"), form.NewOption(\"required\", true), form.NewOption(\"attr\", map[string]string{\"data-foo\": \"foo\"}), form.NewOption(\"row_attr\", map[string]string{\"data-bar\": \"bar\"}), ) \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv data-bar=\"bar\"\u003e \u003clabel for=\"form-name\" \u003eName\u003c/label\u003e \u003cinput id=\"form-name\" required=\"required\" name=\"form[Name]\" value=\"\" type=\"text\" data-foo=\"foo\"\u003e \u003c/div\u003e \u003c/form\u003e "},"title":"Text"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description Info attr map[string]string List of extra attributes ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/fields/":{"data":{"":"The documentation of field is realised with this program.\nimport ( \"bytes\" \"fmt\" \"html/template\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { myForm := form.NewForm( // the documented field here // form.NewField... ). WithName(\"form\"). End() render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse(`{{- form .Form -}}`) var buffer bytes.Buffer tpl.Execute(\u0026buffer, map[string]any{ \"Form\": myForm, }) fmt.Print(buffer.String()) } ","common-options#Common options":" Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field "},"title":"Fields"},"/go-form/docs/fields/input/mail/":{"data":{"":" ok "},"title":"Mail"},"/go-form/docs/fields/input/number/":{"data":{"":" ok "},"title":"Number"},"/go-form/docs/fields/input/password/":{"data":{"":" ok "},"title":"Password"},"/go-form/docs/fields/input/range/":{"data":{"":" ok "},"title":"Range"},"/go-form/docs/fields/input/text/":{"data":{"":"","basic-example#Basic example":" GOResult form.NewFieldText(\"Name\") \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv \u003e \u003cinput id=\"form-name\" name=\"form[Name]\" value=\"\" type=\"text\" \u003e \u003c/div\u003e \u003c/form\u003e ","fully-featured-example#Fully featured example":" GOResult form.NewFieldText(\"Name\"). WithOptions( form.NewOption(\"label\", \"Name\"), form.NewOption(\"required\", true), form.NewOption(\"attr\", map[string]string{\"data-foo\": \"foo\"}), form.NewOption(\"row_attr\", map[string]string{\"data-bar\": \"bar\"}), ) \u003cform action=\"\" method=\"POST\" \u003e \u003cdiv data-bar=\"bar\"\u003e \u003clabel for=\"form-name\" \u003eName\u003c/label\u003e \u003cinput id=\"form-name\" required=\"required\" name=\"form[Name]\" value=\"\" type=\"text\" data-foo=\"foo\"\u003e \u003c/div\u003e \u003c/form\u003e "},"title":"Text"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From 0ad15cc71727e2bf8071a430bbb08f0cc226fdff Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 22 Jul 2025 21:10:33 +0200 Subject: [PATCH 033/117] feat: add constraints (isodd, iseven) --- validation/iseven.go | 34 ++++++++++++++++++++++++++++++++++ validation/isodd.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 validation/iseven.go create mode 100644 validation/isodd.go diff --git a/validation/iseven.go b/validation/iseven.go new file mode 100644 index 0000000..306cae2 --- /dev/null +++ b/validation/iseven.go @@ -0,0 +1,34 @@ +package validation + +import ( + "strconv" +) + +type IsEven struct { + Message string + TypeErrorMessage string +} + +func NewIsEven() IsEven { + return IsEven{ + Message: "This value is not an even number.", + TypeErrorMessage: "This value can not be processed.", + } +} + +func (c IsEven) Validate(data any) []Error { + errors := []Error{} + + // The constraint should not validate an empty data + if len(NewNotBlank().Validate(data)) == 0 { + i, err := strconv.Atoi(data.(string)) + + if err != nil { + errors = append(errors, Error(c.TypeErrorMessage)) + } else if i%2 != 0 { + errors = append(errors, Error(c.Message)) + } + } + + return errors +} diff --git a/validation/isodd.go b/validation/isodd.go new file mode 100644 index 0000000..d7e3b53 --- /dev/null +++ b/validation/isodd.go @@ -0,0 +1,34 @@ +package validation + +import ( + "strconv" +) + +type IsOdd struct { + Message string + TypeErrorMessage string +} + +func NewIsOdd() IsOdd { + return IsOdd{ + Message: "This value is not a odd number.", + TypeErrorMessage: "This value can not be processed.", + } +} + +func (c IsOdd) Validate(data any) []Error { + errors := []Error{} + + // The constraint should not validate an empty data + if len(NewNotBlank().Validate(data)) == 0 { + i, err := strconv.Atoi(data.(string)) + + if err != nil { + errors = append(errors, Error(c.TypeErrorMessage)) + } else if i%2 != 1 { + errors = append(errors, Error(c.Message)) + } + } + + return errors +} From 3fa0268323d078eb0ebab948894bc400aba6b379 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 22 Jul 2025 21:10:58 +0200 Subject: [PATCH 034/117] refactor: refactor constraint (not blank check) --- validation/length.go | 2 +- validation/mail.go | 15 +++++---------- validation/range.go | 2 +- validation/regex.go | 34 +++++++++++++++------------------- 4 files changed, 22 insertions(+), 31 deletions(-) diff --git a/validation/length.go b/validation/length.go index b8ba699..66d5a1f 100644 --- a/validation/length.go +++ b/validation/length.go @@ -60,7 +60,7 @@ func (c Length) WithExact(v int) Length { } func (c Length) Validate(data any) []Error { - if c.Min == nil && c.Max == nil { + if (c.Min == nil && c.Max == nil) || len(NewNotBlank().Validate(data)) != 0 { return []Error{} } diff --git a/validation/mail.go b/validation/mail.go index 53be87a..136ee6d 100644 --- a/validation/mail.go +++ b/validation/mail.go @@ -30,17 +30,12 @@ func NewMail() Mail { func (c Mail) Validate(data any) []Error { errors := []Error{} - notBlank := NotBlank{} - nbErrs := notBlank.Validate(data) + if len(NewNotBlank().Validate(data)) == 0 { + _, err := mail.ParseAddress(data.(string)) - if len(nbErrs) > 0 { - return errors - } - - _, err := mail.ParseAddress(data.(string)) - - if err != nil { - errors = append(errors, Error(c.Message)) + if err != nil { + errors = append(errors, Error(c.Message)) + } } return errors diff --git a/validation/range.go b/validation/range.go index ab0af11..ccb95b0 100644 --- a/validation/range.go +++ b/validation/range.go @@ -60,7 +60,7 @@ func (c Range) WithRange(vMin, vMax float64) Range { } func (c Range) Validate(data any) []Error { - if c.Min == nil && c.Max == nil { + if c.Min == nil && c.Max == nil || len(NewNotBlank().Validate(data)) != 0 { return []Error{} } diff --git a/validation/regex.go b/validation/regex.go index 7e489b0..28815b1 100644 --- a/validation/regex.go +++ b/validation/regex.go @@ -50,29 +50,25 @@ func (c Regex) MustNotMatch() Regex { func (c Regex) Validate(data any) []Error { errors := []Error{} - notBlank := NotBlank{} - nbErrs := notBlank.Validate(data) - if len(nbErrs) > 0 { - return errors - } + if len(NewNotBlank().Validate(data)) == 0 { + t := reflect.TypeOf(data) - t := reflect.TypeOf(data) - - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - switch t.Kind() { - case reflect.String: - matched, _ := regexp.MatchString(c.Expression, data.(string)) - - if !matched && c.Match || matched && !c.Match { - errors = append(errors, Error(c.Message)) + if t.Kind() == reflect.Ptr { + t = t.Elem() } - default: - errors = append(errors, Error(c.TypeErrorMessage)) + switch t.Kind() { + case reflect.String: + matched, _ := regexp.MatchString(c.Expression, data.(string)) + + if !matched && c.Match || matched && !c.Match { + errors = append(errors, Error(c.Message)) + } + + default: + errors = append(errors, Error(c.TypeErrorMessage)) + } } return errors From 833a144437090dcf796df19ae0273b7fe45e53aa Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 22 Jul 2025 19:34:59 +0000 Subject: [PATCH 035/117] Build doc --- categories/index.html | 107 +-- docs/constraints/index.html | 443 ++++++------ docs/fields/button/index.html | 521 -------------- docs/fields/index.html | 943 +++++++++++++++++++------- docs/fields/index.xml | 270 -------- docs/fields/input/date/index.html | 521 -------------- docs/fields/input/datetime/index.html | 521 -------------- docs/fields/input/hidden/index.html | 521 -------------- docs/fields/input/mail/index.html | 531 --------------- docs/fields/input/number/index.html | 531 --------------- docs/fields/input/password/index.html | 531 --------------- docs/fields/input/range/index.html | 531 --------------- docs/fields/input/text/index.html | 713 ------------------- docs/fields/input/time/index.html | 517 -------------- docs/fields/textarea/index.html | 521 -------------- docs/form/index.html | 208 +----- docs/index.html | 208 +----- docs/index.xml | 270 -------- docs/installation/index.html | 208 +----- docs/rendering/index.html | 208 +----- docs/rendering/theming/index.html | 208 +----- docs/workflow/index.html | 208 +----- en.search-data.json | 2 +- index.html | 107 +-- sitemap.xml | 24 +- tags/index.html | 107 +-- 26 files changed, 1042 insertions(+), 8438 deletions(-) delete mode 100644 docs/fields/button/index.html delete mode 100644 docs/fields/input/date/index.html delete mode 100644 docs/fields/input/datetime/index.html delete mode 100644 docs/fields/input/hidden/index.html delete mode 100644 docs/fields/input/mail/index.html delete mode 100644 docs/fields/input/number/index.html delete mode 100644 docs/fields/input/password/index.html delete mode 100644 docs/fields/input/range/index.html delete mode 100644 docs/fields/input/text/index.html delete mode 100644 docs/fields/input/time/index.html delete mode 100644 docs/fields/textarea/index.html diff --git a/categories/index.html b/categories/index.html index 2943a52..d8abfe1 100644 --- a/categories/index.html +++ b/categories/index.html @@ -168,6 +168,14 @@ href="/go-form/docs/form/" >Form + + +
  • Fields
  • Fields - - - -
  • Rendering diff --git a/docs/constraints/index.html b/docs/constraints/index.html index 4e22565..ed948cb 100644 --- a/docs/constraints/index.html +++ b/docs/constraints/index.html @@ -11,7 +11,8 @@ Constraints – deblan/go-form - + @@ -19,10 +20,13 @@ - + + - + @@ -168,6 +172,14 @@ href="/go-form/docs/form/" >Form + + +
  • Fields
  • Constraints + +
  • Fields - - - -
  • Rendering @@ -323,6 +256,13 @@ href="/go-form/docs/form/" >Form +
  • +
  • Fields
  • Fields - -
  • -
  • Rendering @@ -464,8 +306,40 @@
  • Fields + + + + +
  • Constraints -
  • Fields - - - - - -
  • Form +
  • +
  • Fields
  • Fields - -
  • On this page

    @@ -504,36 +478,26 @@ import ( "bytes" "fmt" "html/template" "gitnet.fr/de

    Fields

    -

    The documentation of field is realised with this program.

    -
    +

    A field represents a field in a form.

    +

    Struct +

    -
    import (
    -	"bytes"
    -	"fmt"
    -	"html/template"
    -
    -	"gitnet.fr/deblan/go-form/form"
    -	"gitnet.fr/deblan/go-form/theme"
    -)
    -
    -func main() {
    -	myForm := form.NewForm(
    -		// the documented field here
    -        // form.NewField...
    -	).
    -		WithName("form").
    -		End()
    -
    -	render := theme.NewRenderer(theme.Html5)
    -	tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse(`{{- form .Form -}}`)
    -
    -	var buffer bytes.Buffer
    -
    -	tpl.Execute(&buffer, map[string]any{
    -		"Form": myForm,
    -	})
    -
    -	fmt.Print(buffer.String())
    +
    type Field struct {
    +	Name        string
    +	Widget      string
    +	Data        any
    +	Options     []*Option
    +	Children    []*Field
    +	Constraints []validation.Constraint
    +	Errors      []validation.Error
    +	PrepareView func() map[string]any
    +	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
     }
    -

    Common options -

    +

    Fields +

    + +
    func NewField(name, widget string) *Field
    + +
    +
    +

    Generates a new field with default properties It should not be used directly but inside function like in form.NewFieldText

    +

    Checkbox +

    + +
    func NewFieldCheckbox(name string) *Field
    + +
    +
    +

    Generates an input[type=checkbox]

    +

    Choice +

    + +
    func NewFieldChoice(name string) *Field
    + +
    +
    +

    Generates inputs (checkbox or radio) or selects

    +

    Csrf +

    + +
    func NewFieldCsrf(name string) *Field
    + +
    +
    +

    Date +

    + +
    func NewFieldDate(name string) *Field
    + +
    +
    +

    Generates an input[type=date] with default transformers

    +

    Datetime +

    + +
    func NewFieldDatetime(name string) *Field
    + +
    +
    +

    Generates an input[type=datetime] with default transformers

    +

    DatetimeLocal +

    + +
    func NewFieldDatetimeLocal(name string) *Field
    + +
    +
    +

    Generates an input[type=datetime-local] with default transformers

    +

    Hidden +

    + +
    func NewFieldHidden(name string) *Field
    + +
    +
    +

    Generates an input[type=hidden]

    +

    Mail +

    + +
    func NewFieldMail(name string) *Field
    + +
    +
    +

    Generates an input[type=email]

    +

    Number +

    + +
    func NewFieldNumber(name string) *Field
    + +
    +
    +

    Generates an input[type=number] with default transformers

    +

    Password +

    + +
    func NewFieldPassword(name string) *Field
    + +
    +
    +

    Generates an input[type=password]

    +

    Range +

    + +
    func NewFieldRange(name string) *Field
    + +
    +
    +

    Generates an input[type=range]

    +

    Sub Form +

    + +
    func NewFieldSubForm(name string) *Field
    + +
    +
    +

    Alias:

    +
    + +
    func NewSubForm(name string) *Field
    + +
    +
    +

    Generates a sub form

    +

    Text +

    + +
    func NewFieldText(name string) *Field
    + +
    +
    +

    Generates an input[type=text]

    +

    Textarea +

    + +
    func NewFieldTextarea(name string) *Field
    + +
    +
    +

    Generates a textarea

    +

    Time +

    + +
    func NewFieldTime(name string) *Field
    + +
    +
    +

    Generates an input[type=time] with default transformers

    +

    Submit +

    + +
    func NewSubmit(name string) *Field
    + +
    +
    +

    Generates an input[type=submit]

    +

    Methods +

    Add +

    + +
    func (f *Field) Add(children ...*Field) *Field
    + +
    +
    +

    Appends children

    +

    Bind +

    + +
    func (f *Field) Bind(data map[string]any, key *string) error
    + +
    +
    +

    Bind the data into the given map

    +

    GetChild +

    + +
    func (f *Field) GetChild(name string) *Field
    + +
    +
    +

    Returns a child using its name

    +

    GetId +

    + +
    func (f *Field) GetId() string
    + +
    +
    +

    Computes the id of the field

    +

    GetName +

    + +
    func (f *Field) GetName() string
    + +
    +
    +

    Computes the name of the field

    +

    GetOption +

    + +
    func (f *Field) GetOption(name string) *Option
    + +
    +
    +

    Returns an option using its name

    +

    HasChild +

    + +
    func (f *Field) HasChild(name string) bool
    + +
    +
    +

    Checks if the field contains a child using its name

    +

    HasOption +

    + +
    func (f *Field) HasOption(name string) bool
    + +
    +
    +

    Checks if the field contains an option using its name

    +

    Mount +

    + +
    func (f *Field) Mount(data any) error
    + +
    +
    +

    Populates the field with data

    +

    ResetErrors +

    + +
    func (f *Field) ResetErrors() *Field
    + +
    +
    +

    Resets the field errors

    +

    WithBeforeBind +

    + +
    func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field
    + +
    +
    +

    Sets a transformer applied to the data of a field before defining it in a structure

    +

    WithBeforeMount +

    + +
    func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field
    + +
    +
    +

    Sets a transformer applied to the structure data before displaying it in a field

    +

    WithConstraints +

    + +
    func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field
    + +
    +
    +

    Appends constraints

    +

    WithData +

    + +
    func (f *Field) WithData(data any) *Field
    + +
    +
    +

    Sets data the field

    +

    WithFixedName +

    + +
    func (f *Field) WithFixedName() *Field
    + +
    +
    +

    Sets that the name of the field is not computed

    +

    WithOptions +

    + +
    func (f *Field) WithOptions(options ...*Option) *Field
    + +
    +
    +

    Common options +

    @@ -593,6 +1031,21 @@ import ( "bytes" "fmt" "html/template" "gitnet.fr/de
    Name
    +

    Appends options to the field

    +

    WithSlice +

    + +
    func (f *Field) WithSlice() *Field
    + +
    +
    +

    Sets that the field represents a data slice

    diff --git a/docs/fields/index.xml b/docs/fields/index.xml index a0fbda6..e0f00a2 100644 --- a/docs/fields/index.xml +++ b/docs/fields/index.xml @@ -14,275 +14,5 @@ - - Text - https://deblan.gitnet.page/go-form/docs/fields/input/text/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/text/ - - - - <h2>Basic example<span class="hx-absolute -hx-mt-20" id="basic-example"></span> - <a href="#basic-example" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-scrollbar hx-overflow-x-auto hx-overflow-y-hidden hx-overscroll-x-contain"> - <div class="hx-mt-4 hx-flex hx-w-max hx-min-w-full hx-border-b hx-border-gray-200 hx-pb-px dark:hx-border-neutral-800"><button - class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" - role="tab" - type="button" - aria-controls="tabs-panel-0" aria-selected="true" tabindex="0" data-state="selected">GO</button><button - class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" - role="tab" - type="button" - aria-controls="tabs-panel-1">Result</button></div> -</div> -<div> - <div - class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" - id="tabs-panel-0" - role="tabpanel" tabindex="0" data-state="selected" ><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="nx">form</span><span class="p">.</span><span class="nf">NewFieldText</span><span class="p">(</span><span class="s">&#34;Name&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> -</div> - <div - class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" - id="tabs-panel-1" - role="tabpanel"><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;POST&#34;</span> <span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">div</span> <span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;form[Name]&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> -</div> -</div> -<h2>Fully featured example<span class="hx-absolute -hx-mt-20" id="fully-featured-example"></span> - <a href="#fully-featured-example" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-scrollbar hx-overflow-x-auto hx-overflow-y-hidden hx-overscroll-x-contain"> - <div class="hx-mt-4 hx-flex hx-w-max hx-min-w-full hx-border-b hx-border-gray-200 hx-pb-px dark:hx-border-neutral-800"><button - class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" - role="tab" - type="button" - aria-controls="tabs-panel-0" aria-selected="true" tabindex="0" data-state="selected">GO</button><button - class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" - role="tab" - type="button" - aria-controls="tabs-panel-1">Result</button></div> -</div> -<div> - <div - class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" - id="tabs-panel-0" - role="tabpanel" tabindex="0" data-state="selected" ><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="nx">form</span><span class="p">.</span><span class="nf">NewFieldText</span><span class="p">(</span><span class="s">&#34;Name&#34;</span><span class="p">).</span> -</span></span><span class="line"><span class="cl"> <span class="nf">WithOptions</span><span class="p">(</span> -</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;label&#34;</span><span class="p">,</span> <span class="s">&#34;Name&#34;</span><span class="p">),</span> -</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;required&#34;</span><span class="p">,</span> <span class="kc">true</span><span class="p">),</span> -</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;attr&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;data-foo&#34;</span><span class="p">:</span> <span class="s">&#34;foo&#34;</span><span class="p">}),</span> -</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;row_attr&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;data-bar&#34;</span><span class="p">:</span> <span class="s">&#34;bar&#34;</span><span class="p">}),</span> -</span></span><span class="line"><span class="cl"> <span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> -</div> - <div - class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" - id="tabs-panel-1" - role="tabpanel"><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;POST&#34;</span> <span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">data-bar</span><span class="o">=</span><span class="s">&#34;bar&#34;</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="p">&gt;</span>Name<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="na">required</span><span class="o">=</span><span class="s">&#34;required&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;form[Name]&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">data-foo</span><span class="o">=</span><span class="s">&#34;foo&#34;</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> -</div> -</div> - - - - - - Hidden - https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ - - - - - - - - - - https://deblan.gitnet.page/go-form/docs/fields/button/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/button/ - - - - - - - - - - https://deblan.gitnet.page/go-form/docs/fields/textarea/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/textarea/ - - - - - - - - - Date - https://deblan.gitnet.page/go-form/docs/fields/input/date/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/date/ - - - - - - - - - Datetime - https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ - - - - - - - - - Mail - https://deblan.gitnet.page/go-form/docs/fields/input/mail/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/mail/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - - - Number - https://deblan.gitnet.page/go-form/docs/fields/input/number/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/number/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - - - Password - https://deblan.gitnet.page/go-form/docs/fields/input/password/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/password/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - - - Range - https://deblan.gitnet.page/go-form/docs/fields/input/range/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/range/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - - - Time - https://deblan.gitnet.page/go-form/docs/fields/input/time/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/time/ - - - - - - - diff --git a/docs/fields/input/date/index.html b/docs/fields/input/date/index.html deleted file mode 100644 index 445f1c7..0000000 --- a/docs/fields/input/date/index.html +++ /dev/null @@ -1,521 +0,0 @@ - - - - - - - - - - - - -Date – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/fields/input/datetime/index.html b/docs/fields/input/datetime/index.html deleted file mode 100644 index 1347ad0..0000000 --- a/docs/fields/input/datetime/index.html +++ /dev/null @@ -1,521 +0,0 @@ - - - - - - - - - - - - -Datetime – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/fields/input/hidden/index.html b/docs/fields/input/hidden/index.html deleted file mode 100644 index f28d15e..0000000 --- a/docs/fields/input/hidden/index.html +++ /dev/null @@ -1,521 +0,0 @@ - - - - - - - - - - - - -Hidden – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/fields/input/mail/index.html b/docs/fields/input/mail/index.html deleted file mode 100644 index 56a40fe..0000000 --- a/docs/fields/input/mail/index.html +++ /dev/null @@ -1,531 +0,0 @@ - - - - - - - - - - - - -Mail – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/fields/input/number/index.html b/docs/fields/input/number/index.html deleted file mode 100644 index f178fbb..0000000 --- a/docs/fields/input/number/index.html +++ /dev/null @@ -1,531 +0,0 @@ - - - - - - - - - - - - -Number – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/fields/input/password/index.html b/docs/fields/input/password/index.html deleted file mode 100644 index c4ff620..0000000 --- a/docs/fields/input/password/index.html +++ /dev/null @@ -1,531 +0,0 @@ - - - - - - - - - - - - -Password – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/fields/input/range/index.html b/docs/fields/input/range/index.html deleted file mode 100644 index cec9346..0000000 --- a/docs/fields/input/range/index.html +++ /dev/null @@ -1,531 +0,0 @@ - - - - - - - - - - - - -Range – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/fields/input/text/index.html b/docs/fields/input/text/index.html deleted file mode 100644 index 8222a90..0000000 --- a/docs/fields/input/text/index.html +++ /dev/null @@ -1,713 +0,0 @@ - - - - - - - - - - - - -Text – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - -
    -
    - -
    - -
    - Fields -
    Text
    -
    - -
    -

    Text

    -

    Basic example -

    -
    -
    -
    -
    - -
    form.NewFieldText("Name")
    - -
    -
    -
    -
    - -
    <form action="" method="POST" >
    -	<div >
    -		<input id="form-name"   name="form[Name]" value="" type="text" >
    -	</div>
    -</form>
    - -
    -
    -
    -
    -

    Fully featured example -

    -
    -
    -
    -
    - -
    form.NewFieldText("Name").
    -	WithOptions(
    -		form.NewOption("label", "Name"),
    -		form.NewOption("required", true),
    -		form.NewOption("attr", map[string]string{"data-foo": "foo"}),
    -		form.NewOption("row_attr", map[string]string{"data-bar": "bar"}),
    -	)
    - -
    -
    -
    -
    - -
    <form action="" method="POST" >
    -	<div data-bar="bar">
    -		<label for="form-name" >Name</label>
    -		<input id="form-name"  required="required" name="form[Name]" value="" type="text" data-foo="foo">
    -	</div>
    -</form>
    - -
    -
    -
    -
    - -
    -
    - - -
    -
    -
    - -
    - - - - - - diff --git a/docs/fields/input/time/index.html b/docs/fields/input/time/index.html deleted file mode 100644 index 85ac96e..0000000 --- a/docs/fields/input/time/index.html +++ /dev/null @@ -1,517 +0,0 @@ - - - - - - - - - - - - -Time – deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/fields/textarea/index.html b/docs/fields/textarea/index.html deleted file mode 100644 index f3583a0..0000000 --- a/docs/fields/textarea/index.html +++ /dev/null @@ -1,521 +0,0 @@ - - - - - - - - - - - - -deblan/go-form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - -
    - -
    - - - - - - diff --git a/docs/form/index.html b/docs/form/index.html index 5d01da3..5ccf04c 100644 --- a/docs/form/index.html +++ b/docs/form/index.html @@ -192,6 +192,14 @@ +
  • Fields + +
  • Fields - - - -
  • Rendering @@ -348,108 +257,17 @@
  • Constraints + >Fields
  • Fields - -
  • + >Constraints +
  • Form + + +
  • Fields
  • Fields - - - -
  • Rendering @@ -334,108 +243,17 @@ A form builder based on fields declarations and independent of structs Validatio
  • Constraints + >Fields
  • Fields - -
  • + >Constraints +
  • - Text - https://deblan.gitnet.page/go-form/docs/fields/input/text/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/text/ - - - - <h2>Basic example<span class="hx-absolute -hx-mt-20" id="basic-example"></span> - <a href="#basic-example" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-scrollbar hx-overflow-x-auto hx-overflow-y-hidden hx-overscroll-x-contain"> - <div class="hx-mt-4 hx-flex hx-w-max hx-min-w-full hx-border-b hx-border-gray-200 hx-pb-px dark:hx-border-neutral-800"><button - class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" - role="tab" - type="button" - aria-controls="tabs-panel-0" aria-selected="true" tabindex="0" data-state="selected">GO</button><button - class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" - role="tab" - type="button" - aria-controls="tabs-panel-1">Result</button></div> -</div> -<div> - <div - class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" - id="tabs-panel-0" - role="tabpanel" tabindex="0" data-state="selected" ><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="nx">form</span><span class="p">.</span><span class="nf">NewFieldText</span><span class="p">(</span><span class="s">&#34;Name&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> -</div> - <div - class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" - id="tabs-panel-1" - role="tabpanel"><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;POST&#34;</span> <span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">div</span> <span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;form[Name]&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> -</div> -</div> -<h2>Fully featured example<span class="hx-absolute -hx-mt-20" id="fully-featured-example"></span> - <a href="#fully-featured-example" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-scrollbar hx-overflow-x-auto hx-overflow-y-hidden hx-overscroll-x-contain"> - <div class="hx-mt-4 hx-flex hx-w-max hx-min-w-full hx-border-b hx-border-gray-200 hx-pb-px dark:hx-border-neutral-800"><button - class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" - role="tab" - type="button" - aria-controls="tabs-panel-0" aria-selected="true" tabindex="0" data-state="selected">GO</button><button - class="hextra-tabs-toggle data-[state=selected]:hx-border-primary-500 data-[state=selected]:hx-text-primary-600 data-[state=selected]:dark:hx-border-primary-500 data-[state=selected]:dark:hx-text-primary-600 hx-mr-2 hx-rounded-t hx-p-2 hx-font-medium hx-leading-5 hx-transition-colors -hx-mb-0.5 hx-select-none hx-border-b-2 hx-border-transparent hx-text-gray-600 hover:hx-border-gray-200 hover:hx-text-black dark:hx-text-gray-200 dark:hover:hx-border-neutral-800 dark:hover:hx-text-white" - role="tab" - type="button" - aria-controls="tabs-panel-1">Result</button></div> -</div> -<div> - <div - class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" - id="tabs-panel-0" - role="tabpanel" tabindex="0" data-state="selected" ><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="nx">form</span><span class="p">.</span><span class="nf">NewFieldText</span><span class="p">(</span><span class="s">&#34;Name&#34;</span><span class="p">).</span> -</span></span><span class="line"><span class="cl"> <span class="nf">WithOptions</span><span class="p">(</span> -</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;label&#34;</span><span class="p">,</span> <span class="s">&#34;Name&#34;</span><span class="p">),</span> -</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;required&#34;</span><span class="p">,</span> <span class="kc">true</span><span class="p">),</span> -</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;attr&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;data-foo&#34;</span><span class="p">:</span> <span class="s">&#34;foo&#34;</span><span class="p">}),</span> -</span></span><span class="line"><span class="cl"> <span class="nx">form</span><span class="p">.</span><span class="nf">NewOption</span><span class="p">(</span><span class="s">&#34;row_attr&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;data-bar&#34;</span><span class="p">:</span> <span class="s">&#34;bar&#34;</span><span class="p">}),</span> -</span></span><span class="line"><span class="cl"> <span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> -</div> - <div - class="hextra-tabs-panel hx-rounded hx-pt-6 hx-hidden data-[state=selected]:hx-block" - id="tabs-panel-1" - role="tabpanel"><div class="hextra-code-block hx-relative hx-mt-6 first:hx-mt-0 hx-group/code"> - -<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;POST&#34;</span> <span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">data-bar</span><span class="o">=</span><span class="s">&#34;bar&#34;</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="p">&gt;</span>Name<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;form-name&#34;</span> <span class="na">required</span><span class="o">=</span><span class="s">&#34;required&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;form[Name]&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">data-foo</span><span class="o">=</span><span class="s">&#34;foo&#34;</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> -</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx-opacity-0 hx-transition group-hover/code:hx-opacity-100 hx-flex hx-gap-1 hx-absolute hx-m-[11px] hx-right-0 hx-top-0"> - <button - class="hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50" - title="Copy code" - > - <div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div> - <div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div> - </button> -</div> -</div> -</div> -</div> - - - - - - Hidden - https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ - - - - - - - Installation https://deblan.gitnet.page/go-form/docs/installation/ @@ -172,32 +39,6 @@ - - - https://deblan.gitnet.page/go-form/docs/fields/button/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/button/ - - - - - - - - - - https://deblan.gitnet.page/go-form/docs/fields/textarea/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/textarea/ - - - - - - - https://deblan.gitnet.page/go-form/docs/rendering/theming/ @@ -208,117 +49,6 @@ - - - - - Date - https://deblan.gitnet.page/go-form/docs/fields/input/date/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/date/ - - - - - - - - - Datetime - https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ - - - - - - - - - Mail - https://deblan.gitnet.page/go-form/docs/fields/input/mail/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/mail/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - - - Number - https://deblan.gitnet.page/go-form/docs/fields/input/number/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/number/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - - - Password - https://deblan.gitnet.page/go-form/docs/fields/input/password/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/password/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - - - Range - https://deblan.gitnet.page/go-form/docs/fields/input/range/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/range/ - - - - <table> - <tr> - <td>ok</td> - </tr> -</table> - - - - - - Time - https://deblan.gitnet.page/go-form/docs/fields/input/time/ - Mon, 01 Jan 0001 00:00:00 +0000 - - https://deblan.gitnet.page/go-form/docs/fields/input/time/ - - - - diff --git a/docs/installation/index.html b/docs/installation/index.html index 3fb7e88..41530da 100644 --- a/docs/installation/index.html +++ b/docs/installation/index.html @@ -180,6 +180,14 @@ go get gitnet.fr/deblan/go-form href="/go-form/docs/form/" >Form + + +
  • Fields
  • Fields - - - -
  • Rendering @@ -338,108 +247,17 @@ go get gitnet.fr/deblan/go-form
  • Constraints + >Fields
  • Fields - -
  • + >Constraints +
  • Form + + +
  • Fields
  • Constraints -
  • Fields - - -
  • Constraints + >Fields
  • Fields - -
  • + >Constraints +
  • Form + + +
  • Fields
  • Constraints -
  • Fields - - -
  • Constraints + >Fields
  • Fields - -
  • + >Constraints +
  • Form + + +
  • Fields
  • Fields - - - -
  • Rendering @@ -336,108 +245,17 @@
  • Constraints + >Fields
  • Fields - -
  • + >Constraints +
  • Form + + +
  • Fields
  • Fields - - - -
  • Rendering diff --git a/sitemap.xml b/sitemap.xml index dcab1ff..33d3a6e 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,47 +2,25 @@ - https://deblan.gitnet.page/go-form/docs/fields/input/text/ - - https://deblan.gitnet.page/go-form/docs/fields/input/hidden/ - https://deblan.gitnet.page/go-form/docs/installation/ https://deblan.gitnet.page/go-form/docs/workflow/ https://deblan.gitnet.page/go-form/docs/form/ - - https://deblan.gitnet.page/go-form/docs/constraints/ https://deblan.gitnet.page/go-form/docs/fields/ - https://deblan.gitnet.page/go-form/docs/fields/button/ - - https://deblan.gitnet.page/go-form/docs/fields/textarea/ + https://deblan.gitnet.page/go-form/docs/constraints/ https://deblan.gitnet.page/go-form/docs/rendering/ https://deblan.gitnet.page/go-form/docs/rendering/theming/ https://deblan.gitnet.page/go-form/categories/ - - https://deblan.gitnet.page/go-form/docs/fields/input/date/ - - https://deblan.gitnet.page/go-form/docs/fields/input/datetime/ https://deblan.gitnet.page/go-form/docs/ - - https://deblan.gitnet.page/go-form/docs/fields/input/mail/ - - https://deblan.gitnet.page/go-form/docs/fields/input/number/ - - https://deblan.gitnet.page/go-form/docs/fields/input/password/ - - https://deblan.gitnet.page/go-form/docs/fields/input/range/ https://deblan.gitnet.page/go-form/tags/ - - https://deblan.gitnet.page/go-form/docs/fields/input/time/ https://deblan.gitnet.page/go-form/ diff --git a/tags/index.html b/tags/index.html index 76292e7..a421c6f 100644 --- a/tags/index.html +++ b/tags/index.html @@ -168,6 +168,14 @@ href="/go-form/docs/form/" >Form + + +
  • Fields
  • Fields - - - -
  • Rendering From 8172bb800d0e0545e573c9dab86b4f942dc848a1 Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 22 Jul 2025 19:36:48 +0000 Subject: [PATCH 036/117] Build doc --- docs/fields/index.html | 47 ++++-------------------------------------- en.search-data.json | 2 +- 2 files changed, 5 insertions(+), 44 deletions(-) diff --git a/docs/fields/index.html b/docs/fields/index.html index fefed87..c64d09b 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -12,7 +12,7 @@ Fields – deblan/go-form +Fields func NewField(name, widget string) *Field Generates a new field with default properties It should not be used directly but inside function like in form.NewFieldText" /> @@ -21,12 +21,12 @@ Struct type Field struct { Name string Widget string Data any Options []*Option - +Fields func NewField(name, widget string) *Field Generates a new field with default properties It should not be used directly but inside function like in form.NewFieldText"> + +Fields func NewField(name, widget string) *Field Generates a new field with default properties It should not be used directly but inside function like in form.NewFieldText"> @@ -183,12 +183,6 @@ Struct type Field struct { Name string Widget string Data any Options []*Option
    • - Struct -
    • -
    • On this page

        -
      • - Struct - -
      • Fields @@ -479,35 +469,6 @@ Struct type Field struct { Name string Widget string Data any Options []*Option

        Fields

        A field represents a field in a form.

        -

        Struct -

        - -
        type Field struct {
        -	Name        string
        -	Widget      string
        -	Data        any
        -	Options     []*Option
        -	Children    []*Field
        -	Constraints []validation.Constraint
        -	Errors      []validation.Error
        -	PrepareView func() map[string]any
        -	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
        -}
        - -
        -

        Fields

        diff --git a/en.search-data.json b/en.search-data.json index 1e41363..414c44e 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"A field represents a field in a form.","fields#Fields":" func NewField(name, widget string) *Field Generates a new field with default properties It should not be used directly but inside function like in form.NewFieldText\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice","struct#Struct":" type Field struct { Name string Widget string Data any Options []*Option Children []*Field Constraints []validation.Constraint Errors []validation.Error PrepareView func() map[string]any 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 } "},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"A field represents a field in a form.","fields#Fields":" func NewField(name, widget string) *Field Generates a new field with default properties It should not be used directly but inside function like in form.NewFieldText\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From 23dc43ca8c568a8a3d32247e27440731ec0ae4cf Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 22 Jul 2025 19:38:00 +0000 Subject: [PATCH 037/117] Build doc --- docs/fields/index.html | 33 +++++++++------------------------ en.search-data.json | 2 +- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/docs/fields/index.html b/docs/fields/index.html index c64d09b..fe67b0d 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -12,7 +12,8 @@ Fields – deblan/go-form +Checkbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox] +Choice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects" /> @@ -21,12 +22,14 @@ Fields func NewField(name, widget string) *Field Generates a new field with defa - +Checkbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox] +Choice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects"> + +Checkbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox] +Choice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects"> @@ -184,9 +187,9 @@ Fields func NewField(name, widget string) *Field Generates a new field with defa
        • Fields + >
        • On this page

            -
          • - Fields - -
          • Checkbox @@ -469,20 +468,6 @@ Fields func NewField(name, widget string) *Field Generates a new field with defa

            Fields

            A field represents a field in a form.

            -

            Fields -

            - -
            func NewField(name, widget string) *Field
            - -
            -
            -

            Generates a new field with default properties It should not be used directly but inside function like in form.NewFieldText

            Checkbox

            diff --git a/en.search-data.json b/en.search-data.json index 414c44e..b1f9640 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"A field represents a field in a form.","fields#Fields":" func NewField(name, widget string) *Field Generates a new field with default properties It should not be used directly but inside function like in form.NewFieldText\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From e6c2bfad368d3f4fa537cb7c78ccdd9d75762a53 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 23 Jul 2025 08:43:41 +0200 Subject: [PATCH 038/117] update changelog --- CHANGELOG.md | 7 +++++++ example/form.go | 1 + 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722d33e..a746cda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## [Unreleased] +## v1.1.0 + +### Added + +- feat: add constraints (isodd, iseven) +- refactor: refactor constraint (not blank check) + ## v1.0.1 ### Added diff --git a/example/form.go b/example/form.go index 926ee3c..183b723 100644 --- a/example/form.go +++ b/example/form.go @@ -109,6 +109,7 @@ func CreateDataForm() *form.Form { ). WithConstraints( validation.NewRange().WithRange(1, 20), + validation.NewIsEven(), ), form.NewFieldRange("Range"). WithOptions( From 960d9175b12d8278565ff855cd45fa2ab253c485 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Jul 2025 12:48:04 +0200 Subject: [PATCH 039/117] feat: replace templates with components --- form/field_input_date.go | 2 +- form/option.go | 12 + go.mod | 6 + go.sum | 6 + theme/bootstrap5.go | 278 +++++++++++------------ theme/html5.go | 464 ++++++++++++++++++++++++++++++--------- theme/renderer.go | 160 ++------------ theme/theme.go | 16 ++ 8 files changed, 558 insertions(+), 386 deletions(-) create mode 100644 theme/theme.go diff --git a/form/field_input_date.go b/form/field_input_date.go index d18bfd4..893c59d 100644 --- a/form/field_input_date.go +++ b/form/field_input_date.go @@ -37,7 +37,7 @@ func DateBeforeMount(data any, format string) (any, error) { } } - return data, nil + return nil, nil } // Generates an input[type=date] with default transformers diff --git a/form/option.go b/form/option.go index bbd3276..ba483e5 100644 --- a/form/option.go +++ b/form/option.go @@ -26,3 +26,15 @@ func NewOption(name string, value any) *Option { Value: value, } } + +func (o *Option) AsBool() bool { + return o.Value.(bool) +} + +func (o *Option) AsString() string { + return o.Value.(string) +} + +func (o *Option) AsMapString() map[string]string { + return o.Value.(map[string]string) +} diff --git a/go.mod b/go.mod index 2f88d00..0b43ae4 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,9 @@ require ( github.com/spf13/cast v1.9.2 github.com/yassinebenaid/godump v0.11.1 ) + +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 d02abfe..fd3f0f9 100644 --- a/go.sum +++ b/go.sum @@ -10,7 +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/theme/bootstrap5.go b/theme/bootstrap5.go index 6571d9b..6966955 100644 --- a/theme/bootstrap5.go +++ b/theme/bootstrap5.go @@ -15,141 +15,147 @@ package theme // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -var Bootstrap5 = map[string]string{ - "form": `
            - {{- form_error .Form nil -}} +var Bootstrap5 = ExtendTheme(Html5, func() map[string]RenderFunc { + theme := make(map[string]RenderFunc) - {{- form_help .Form -}} + return theme +}) - {{- range $field := .Form.Fields -}} - {{- form_row $field -}} - {{- end -}} -
            `, - "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`, - "help": ` - {{- if gt (len .Help) 0 -}} -
            {{ .Help }}
            - {{- end -}} - `, - "label": ` - {{ if .Field.HasOption "label" }} - {{ $label := (.Field.GetOption "label").Value }} - - {{- if ne $label "" -}} - - {{- end -}} - {{- end -}} - `, - "input": ` - {{- $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 -}} - {{- $class := "form-control" }} - - {{- if eq $type.Value "checkbox" -}} - {{- $value = 1 -}} - {{- end -}} - - {{- if or (eq $type.Value "checkbox") (eq $type.Value "radio") -}} - {{- $class = "form-check-input" -}} - {{- end -}} - - {{- if eq $type.Value "range" -}} - {{- $class = "form-range" -}} - {{- end -}} - - {{- if or (eq $type.Value "submit") (eq $type.Value "reset") (eq $type.Value "button") -}} - {{- $class = "" -}} - - {{ if .Field.HasOption "attr" }} - {{ $class = (.Field.GetOption "attr").Value.attr.class }} - {{ end }} - {{- end -}} - - - `, - "textarea": ` - - `, - "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 -}} - - {{- if and (not $required) (not $isMultiple) -}} - {{- $keyAdd = 1 -}} - {{- end -}} - - {{- if $isExpanded -}} - {{- if and (not $required) (not $isMultiple) -}} -
            - - -
            - {{- end -}} - - {{- range $key, $choice := $choices.GetChoices -}} -
            - - -
            - {{- end -}} - {{- else -}} - - {{- end -}} - `, - "sub_form": ` -
            - {{ if .Field.HasOption "label" }} - {{ $label := (.Field.GetOption "label").Value }} - - {{- if ne $label "" -}} - {{ $label }} - {{- end -}} - {{- end -}} - - {{ form_widget_help .Field }} - - {{- range $field := .Field.Children -}} - {{- form_row $field -}} - {{- end -}} -
            - `, - "error": ` - {{- if gt (len .Errors) 0 -}} -
            - {{- range $error := .Errors -}} -
            {{- $error -}}
            - {{- end -}} -
            - {{- end -}} - `, - "row": `
            - {{ $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_widget .Field -}} - {{- form_error nil .Field -}} - - {{ if and (eq (len .Field.Children) 0) ($labelAfterWidget) }} - {{- form_label .Field -}} - {{ end }} - - {{- form_widget_help .Field -}} -
            `, -} +// var Bootstrap5 = map[string]string{ +// "form": `
            +// {{- form_error .Form nil -}} +// +// {{- form_help .Form -}} +// +// {{- range $field := .Form.Fields -}} +// {{- form_row $field -}} +// {{- end -}} +//
            `, +// "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`, +// "help": ` +// {{- if gt (len .Help) 0 -}} +//
            {{ .Help }}
            +// {{- end -}} +// `, +// "label": ` +// {{ if .Field.HasOption "label" }} +// {{ $label := (.Field.GetOption "label").Value }} +// +// {{- if ne $label "" -}} +// +// {{- end -}} +// {{- end -}} +// `, +// "input": ` +// {{- $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 -}} +// {{- $class := "form-control" }} +// +// {{- if eq $type.Value "checkbox" -}} +// {{- $value = 1 -}} +// {{- end -}} +// +// {{- if or (eq $type.Value "checkbox") (eq $type.Value "radio") -}} +// {{- $class = "form-check-input" -}} +// {{- end -}} +// +// {{- if eq $type.Value "range" -}} +// {{- $class = "form-range" -}} +// {{- end -}} +// +// {{- if or (eq $type.Value "submit") (eq $type.Value "reset") (eq $type.Value "button") -}} +// {{- $class = "" -}} +// +// {{ if .Field.HasOption "attr" }} +// {{ $class = (.Field.GetOption "attr").Value.attr.class }} +// {{ end }} +// {{- end -}} +// +// +// `, +// "textarea": ` +// +// `, +// "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 -}} +// +// {{- if and (not $required) (not $isMultiple) -}} +// {{- $keyAdd = 1 -}} +// {{- end -}} +// +// {{- if $isExpanded -}} +// {{- if and (not $required) (not $isMultiple) -}} +//
            +// +// +//
            +// {{- end -}} +// +// {{- range $key, $choice := $choices.GetChoices -}} +//
            +// +// +//
            +// {{- end -}} +// {{- else -}} +// +// {{- end -}} +// `, +// "sub_form": ` +//
            +// {{ if .Field.HasOption "label" }} +// {{ $label := (.Field.GetOption "label").Value }} +// +// {{- if ne $label "" -}} +// {{ $label }} +// {{- end -}} +// {{- end -}} +// +// {{ form_widget_help .Field }} +// +// {{- range $field := .Field.Children -}} +// {{- form_row $field -}} +// {{- end -}} +//
            +// `, +// "error": ` +// {{- if gt (len .Errors) 0 -}} +//
            +// {{- range $error := .Errors -}} +//
            {{- $error -}}
            +// {{- end -}} +//
            +// {{- end -}} +// `, +// "row": `
            +// {{ $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_widget .Field -}} +// {{- form_error nil .Field -}} +// +// {{ if and (eq (len .Field.Children) 0) ($labelAfterWidget) }} +// {{- form_label .Field -}} +// {{ end }} +// +// {{- form_widget_help .Field -}} +//
            `, +// } diff --git a/theme/html5.go b/theme/html5.go index 718d45b..a3dc337 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -1,5 +1,15 @@ package theme +import ( + "fmt" + + "github.com/spf13/cast" + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/validation" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + // @license GNU AGPL version 3 or any later version // // This program is free software: you can redistribute it and/or modify @@ -15,120 +25,370 @@ package theme // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -var Html5 = map[string]string{ - "form": `
            - {{- form_error .Form nil -}} +var Html5 = CreateTheme(func() map[string]RenderFunc { + theme := make(map[string]RenderFunc) - {{- form_help .Form -}} + theme["attributes"] = func(args ...any) Node { + var result []Node - {{- range $field := .Form.Fields -}} - {{- form_row $field -}} - {{- end -}} -
            `, - "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`, - "help": ` - {{- if gt (len .Help) 0 -}} -
            {{ .Help }}
            - {{- end -}} - `, - "label": ` - {{ if .Field.HasOption "label" }} - {{ $label := (.Field.GetOption "label").Value }} + for i, v := range args[0].(map[string]string) { + result = append(result, Attr(i, v)) + } - {{- if ne $label "" -}} - - {{- end -}} - {{- end -}} - `, - "input": ` - {{- $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 -}} + return Group(result) + } - {{- if eq $type.Value "checkbox" -}} - {{- $value = 1 -}} - {{- end -}} + theme["form_attributes"] = func(args ...any) Node { + form := args[0].(*form.Form) - - `, - "textarea": ` - - `, - "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 -}} + if !form.HasOption("attr") { + return Raw("") + } - {{- if and (not $required) (not $isMultiple) -}} - {{- $keyAdd = 1 -}} - {{- end -}} + return theme["attributes"](form.GetOption("attr").AsMapString()) + } - {{- if $isExpanded -}} - {{- if and (not $required) (not $isMultiple) -}} - - - {{- end -}} + theme["errors"] = func(args ...any) Node { + errors := args[0].([]validation.Error) - {{- range $key, $choice := $choices.GetChoices -}} - - - {{- end -}} - {{- else -}} - - {{- end -}} - `, - "sub_form": ` -
            - {{ if .Field.HasOption "label" }} - {{ $label := (.Field.GetOption "label").Value }} + var result []Node - {{- if ne $label "" -}} - {{ $label }} - {{- end -}} - {{- end -}} + for _, v := range errors { + result = append(result, Li(Text(string(v)))) + } - {{ form_widget_help .Field }} + return Ul( + Class("form-errors"), + Group(result), + ) + } - {{- range $field := .Field.Children -}} - {{- form_row $field -}} - {{- end -}} -
            - `, - "error": ` - {{- if gt (len .Errors) 0 -}} -
              - {{- range $error := .Errors -}} -
            • {{- $error -}}
            • - {{- end -}} -
            - {{- end -}} - `, - "row": `
            - {{ $labelAfterWidget := and (.Field.HasOption "type") (eq (.Field.GetOption "type").Value "checkbox") }} + theme["form_errors"] = func(args ...any) Node { + form := args[0].(*form.Form) - {{ if and (eq (len .Field.Children) 0) (not $labelAfterWidget) }} - {{- form_label .Field -}} - {{ end }} + return If( + len(form.Errors) > 0, + theme["errors"](form.Errors), + ) + } - {{- form_error nil .Field -}} - {{- form_widget .Field -}} + theme["form_widget_errors"] = func(args ...any) Node { + field := args[0].(*form.Field) - {{ if and (eq (len .Field.Children) 0) ($labelAfterWidget) }} - {{- form_label .Field -}} - {{ end }} + return If( + len(field.Errors) > 0, + theme["errors"](field.Errors), + ) + } - {{- form_widget_help .Field -}} -
            `, -} + theme["help"] = func(args ...any) Node { + help := args[0].(string) + + if len(help) == 0 { + return Raw("") + } + + return Div( + Class("form-help"), + Text("ok"), + ) + } + + theme["form_help"] = func(args ...any) Node { + form := args[0].(*form.Form) + + if !form.HasOption("help") { + return Raw("") + } + + return theme["help"](form.GetOption("help").AsString()) + } + + theme["form_widget_help"] = func(args ...any) Node { + field := args[0].(*form.Field) + + if !field.HasOption("help") { + return Raw("") + } + + return theme["help"](field.GetOption("help").AsString()) + } + + theme["label_attributes"] = func(args ...any) Node { + field := args[0].(*form.Field) + + if !field.HasOption("label_attr") { + return Raw("") + } + + return theme["attributes"](field.GetOption("label_attr").AsMapString()) + } + + theme["form_label"] = func(args ...any) Node { + field := args[0].(*form.Field) + + if !field.HasOption("label") { + return Raw("") + } + + label := field.GetOption("label").AsString() + + return If(len(label) > 0, Label( + Class("form-label"), + For(field.GetId()), + theme["label_attributes"](field), + Text(label), + )) + } + + theme["field_attributes"] = func(args ...any) Node { + field := args[0].(*form.Field) + + if !field.HasOption("attr") { + return Raw("") + } + + return theme["attributes"](field.GetOption("attr").AsMapString()) + } + + theme["textarea_attributes"] = func(args ...any) Node { + return theme["field_attributes"](args...) + } + + theme["input_attributes"] = func(args ...any) Node { + return theme["field_attributes"](args...) + } + + theme["sub_form_attributes"] = func(args ...any) Node { + return theme["field_attributes"](args...) + } + + theme["input"] = func(args ...any) Node { + field := args[0].(*form.Field) + fieldType := "text" + + if field.HasOption("type") { + fieldType = field.GetOption("type").AsString() + } + + value := cast.ToString(field.Data) + + if fieldType == "checkbox" { + value = "1" + } + + return Input( + Name(field.GetName()), + ID(field.GetId()), + Type(fieldType), + Value(value), + If(fieldType == "checkbox" && field.Data != false, Checked()), + If(field.HasOption("required") && field.GetOption("required").AsBool(), Required()), + theme["input_attributes"](field), + ) + } + + theme["choice_options"] = func(args ...any) Node { + field := args[0].(*form.Field) + choices := field.GetOption("choices").Value.(*form.Choices) + + isRequired := field.HasOption("required") && field.GetOption("required").AsBool() + isMultiple := field.GetOption("multiple").AsBool() + + var options []Node + + if !isMultiple && !isRequired { + options = append(options, Option( + Text(field.GetOption("empty_choice_label").AsString()), + )) + } + + for _, choice := range choices.GetChoices() { + options = append(options, Option( + Value(choice.Value), + Text(choice.Label), + If(choices.Match(field, choice.Value), Selected()), + )) + } + + return Group(options) + } + + theme["choice_expanded"] = func(args ...any) Node { + field := args[0].(*form.Field) + choices := field.GetOption("choices").Value.(*form.Choices) + + isRequired := field.HasOption("required") && field.GetOption("required").AsBool() + isMultiple := field.GetOption("multiple").AsBool() + noneLabel := field.GetOption("empty_choice_label").AsString() + + var items []Node + + if !isMultiple && !isRequired { + id := fmt.Sprintf("%s-%s", field.GetId(), "none") + + items = append(items, Group([]Node{ + Input( + Name(field.GetName()), + ID(id), + Value(""), + Type("radio"), + theme["input_attributes"](field), + If(cast.ToString(field.Data) == "", Checked()), + ), + Label(For(id), Text(noneLabel)), + })) + } + + for key, choice := range choices.GetChoices() { + id := fmt.Sprintf("%s-%d", field.GetId(), key) + + items = append(items, Group([]Node{ + Input( + Name(field.GetName()), + ID(id), + Value(choice.Value), + If(isMultiple, Type("checkbox")), + If(!isMultiple, Type("radio")), + theme["input_attributes"](field), + If(choices.Match(field, choice.Value), Checked()), + ), + Label(For(id), Text(choice.Label)), + })) + } + + return Group(items) + } + + theme["choice"] = func(args ...any) Node { + field := args[0].(*form.Field) + + isRequired := field.HasOption("required") && field.GetOption("required").AsBool() + isExpanded := field.GetOption("expanded").AsBool() + isMultiple := field.GetOption("multiple").AsBool() + noneLabel := field.GetOption("empty_choice_label").AsString() + + _ = noneLabel + + if isExpanded { + return theme["choice_expanded"](field) + } else { + return Select( + ID(field.GetId()), + If(isRequired, Required()), + If(isMultiple, Multiple()), + Name(field.GetName()), + theme["choice_options"](field), + ) + } + } + + theme["textarea"] = func(args ...any) Node { + field := args[0].(*form.Field) + + return Textarea( + Name(field.GetName()), + ID(field.GetId()), + If(field.HasOption("required") && field.GetOption("required").AsBool(), Required()), + theme["textarea_attributes"](field), + Text(cast.ToString(field.Data)), + ) + } + + theme["sub_form_label"] = func(args ...any) Node { + field := args[0].(*form.Field) + + if !field.HasOption("label") { + return Raw("") + } + + label := field.GetOption("label").AsString() + + return If(len(label) > 0, Legend( + Class("form-label"), + theme["label_attributes"](field), + Text(label), + )) + + } + + theme["sub_form_content"] = func(args ...any) Node { + field := args[0].(*form.Field) + + return theme["form_fields"](field.Children) + } + + theme["sub_form"] = func(args ...any) Node { + field := args[0].(*form.Field) + + return FieldSet( + ID(field.GetId()), + theme["sub_form_label"](field), + theme["sub_form_attributes"](field), + theme["sub_form_content"](field), + ) + } + + theme["form_widget"] = func(args ...any) Node { + field := args[0].(*form.Field) + + tpl, ok := theme[field.Widget] + + if !ok { + return Raw("Invalid field widget: " + field.Widget) + } + + return tpl(field) + } + + theme["form_row"] = func(args ...any) Node { + field := args[0].(*form.Field) + + isCheckbox := field.HasOption("type") && field.GetOption("type").AsString() == "checkbox" + hasChildren := len(field.Children) > 0 + labelAfter := isCheckbox && !hasChildren + label := theme["form_label"](field) + + return Div( + Class("form-row"), + If(!labelAfter, label), + theme["form_widget_errors"](field), + theme["form_widget"](field), + If(labelAfter, label), + theme["form_widget_help"](field), + ) + } + + theme["form_fields"] = func(args ...any) Node { + var items []Node + + for _, item := range args[0].([]*form.Field) { + items = append(items, theme["form_row"](item)) + } + + return Group(items) + } + + theme["form_content"] = func(args ...any) Node { + form := args[0].(*form.Form) + + return Div( + theme["form_errors"](form), + theme["form_help"](form), + theme["form_fields"](form.Fields), + ) + } + + theme["form"] = func(args ...any) Node { + form := args[0].(*form.Form) + + return Form( + Action(form.Action), + Method(form.Method), + theme["form_attributes"](form), + theme["form_content"](form), + ) + } + + return theme +}) diff --git a/theme/renderer.go b/theme/renderer.go index b572f45..98cbd51 100644 --- a/theme/renderer.go +++ b/theme/renderer.go @@ -19,172 +19,38 @@ import ( "bytes" "html/template" - "github.com/spf13/cast" "gitnet.fr/deblan/go-form/form" - "gitnet.fr/deblan/go-form/validation" + "maragu.dev/gomponents" ) +type RenderFunc func(args ...any) gomponents.Node + type Renderer struct { - Theme map[string]string + Theme map[string]RenderFunc } -func NewRenderer(theme map[string]string) *Renderer { +func NewRenderer(theme map[string]RenderFunc) *Renderer { r := new(Renderer) r.Theme = theme return r } -func (r *Renderer) RenderForm(form *form.Form) template.HTML { - return r.Render("form", r.Theme["form"], map[string]any{ - "Form": form, - }) -} - -func (r *Renderer) RenderRow(field *form.Field) template.HTML { - return r.Render("row", r.Theme["row"], map[string]any{ - "Field": field, - }) -} - -func (r *Renderer) RenderLabel(field *form.Field) template.HTML { - return r.Render("label", r.Theme["label"], map[string]any{ - "Field": field, - }) -} - -func (r *Renderer) RenderWidget(field *form.Field) template.HTML { - return r.Render("widget", r.Theme[field.Widget], map[string]any{ - "Field": field, - }) -} - -func (r *Renderer) RenderError(form *form.Form, field *form.Field) template.HTML { - var errors []validation.Error - - if field != nil { - errors = field.Errors - } - - if form != nil { - errors = form.Errors - } - - return r.Render("error", r.Theme["error"], map[string]any{ - "Errors": errors, - }) -} - -func (r *Renderer) RenderLabelAttr(field *form.Field) template.HTMLAttr { - var attributes map[string]string - - if field.HasOption("label_attr") { - attributes = field.GetOption("label_attr").Value.(map[string]string) - } - - return r.RenderAttr("label_attr", r.Theme["attributes"], map[string]any{ - "Attributes": attributes, - }) -} - -func (r *Renderer) RenderWidgetAttr(field *form.Field) template.HTMLAttr { - var attributes map[string]string - - if field.HasOption("attr") { - attributes = field.GetOption("attr").Value.(map[string]string) - } - - return r.RenderAttr("widget_attr", r.Theme["attributes"], map[string]any{ - "Attributes": attributes, - }) -} - -func (r *Renderer) RenderFormAttr(form *form.Form) template.HTMLAttr { - var attributes map[string]string - - if form.HasOption("attr") { - attributes = form.GetOption("attr").Value.(map[string]string) - } - - return r.RenderAttr("form_attr", r.Theme["attributes"], map[string]any{ - "Attributes": attributes, - }) -} - -func (r *Renderer) RenderRowAttr(field *form.Field) template.HTMLAttr { - var attributes map[string]string - - if field.HasOption("row_attr") { - attributes = field.GetOption("row_attr").Value.(map[string]string) - } - - return r.RenderAttr("raw_attr", r.Theme["attributes"], map[string]any{ - "Attributes": attributes, - }) -} - -func (r *Renderer) RenderFormHelp(form *form.Form) template.HTML { - var help string - - if form.HasOption("help") { - help = form.GetOption("help").Value.(string) - } - - return r.Render("help", r.Theme["help"], map[string]any{ - "Help": help, - }) -} - -func (r *Renderer) RenderWidgetHelp(field *form.Field) template.HTML { - var help string - - if field.HasOption("help") { - help = field.GetOption("help").Value.(string) - } - - return r.Render("help", r.Theme["help"], map[string]any{ - "Help": help, - }) -} - -func (r *Renderer) RenderAttr(name, tpl string, args any) template.HTMLAttr { - t, err := template.New(name).Parse(tpl) - - if err != nil { - return template.HTMLAttr("") - } - +func toTemplateHtml(n gomponents.Node) template.HTML { var buf bytes.Buffer - err = t.Execute(&buf, args) - if err != nil { - return template.HTMLAttr("") - } + n.Render(&buf) - return template.HTMLAttr(buf.String()) + return template.HTML(buf.String()) +} + +func (r *Renderer) RenderForm(form *form.Form) template.HTML { + return toTemplateHtml(r.Theme["form"](form)) } func (r *Renderer) FuncMap() template.FuncMap { return template.FuncMap{ - "form": r.RenderForm, - "form_row": r.RenderRow, - "form_label": r.RenderLabel, - "form_widget": r.RenderWidget, - "form_error": r.RenderError, - "form_attr": r.RenderFormAttr, - "form_widget_attr": r.RenderWidgetAttr, - "form_label_attr": r.RenderLabelAttr, - "form_row_attr": r.RenderRowAttr, - "form_help": r.RenderFormHelp, - "form_widget_help": r.RenderWidgetHelp, - "sum": func(values ...any) float64 { - res := float64(0) - for _, value := range values { - res += cast.ToFloat64(value) - } - - return res - }, + "form": r.RenderForm, } } diff --git a/theme/theme.go b/theme/theme.go new file mode 100644 index 0000000..e63864e --- /dev/null +++ b/theme/theme.go @@ -0,0 +1,16 @@ +package theme + +func CreateTheme(generator func() map[string]RenderFunc) map[string]RenderFunc { + return generator() +} + +func ExtendTheme(base map[string]RenderFunc, generator func() map[string]RenderFunc) map[string]RenderFunc { + extended := CreateTheme(generator) + + for i, v := range base { + extended[i] = v + extended["base_"+i] = v + } + + return extended +} From 0cd313f110bcfc0638ab7d71b7885a0bb579610f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Jul 2025 19:48:15 +0200 Subject: [PATCH 040/117] feat: add boostrap5 theme feat: replace map[string]string with form.Attr in options --- doc.md | 1 + example/form.go | 38 ++++--- form/field.go | 7 ++ form/option.go | 42 +++++++- theme/bootstrap5.go | 247 +++++++++++++++++++------------------------- theme/html5.go | 188 ++++++++++++++++++--------------- theme/renderer.go | 4 +- theme/theme.go | 9 +- 8 files changed, 292 insertions(+), 244 deletions(-) create mode 100644 doc.md diff --git a/doc.md b/doc.md new file mode 100644 index 0000000..e08a3ab --- /dev/null +++ b/doc.md @@ -0,0 +1 @@ +go doc -all -short form.Form | sed 's/^func \(.*\)$/### \0\n\n\`\`\`golang\n\0\n\`\`\`/g' | sed 's/^ \(.*\)/\n\1/g' | sed 's/^### func .* \(.*\)\((.*)\).*/### \1/' | xclip -sel c diff --git a/example/form.go b/example/form.go index 183b723..d945a99 100644 --- a/example/form.go +++ b/example/form.go @@ -64,7 +64,7 @@ func CreateDataForm() *form.Form { WithOptions( form.NewOption("label", "Bytes"), form.NewOption("required", true), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ). @@ -81,7 +81,7 @@ func CreateDataForm() *form.Form { WithOptions( form.NewOption("label", "Text"), form.NewOption("help", "Must contain 'deblan'"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ). @@ -91,7 +91,7 @@ func CreateDataForm() *form.Form { form.NewFieldCheckbox("Checkbox"). WithOptions( form.NewOption("label", "Checkbox"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ), @@ -103,7 +103,7 @@ func CreateDataForm() *form.Form { form.NewFieldNumber("Number"). WithOptions( form.NewOption("label", "Number"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ). @@ -114,14 +114,14 @@ func CreateDataForm() *form.Form { form.NewFieldRange("Range"). WithOptions( form.NewOption("label", "Range"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ), form.NewFieldMail("Mail"). WithOptions( form.NewOption("label", "Mail"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ). @@ -131,7 +131,7 @@ func CreateDataForm() *form.Form { form.NewFieldPassword("Password"). WithOptions( form.NewOption("label", "Password"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ). @@ -147,28 +147,28 @@ func CreateDataForm() *form.Form { form.NewFieldDate("Date"). WithOptions( form.NewOption("label", "Date"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ), form.NewFieldDatetime("DateTime"). WithOptions( form.NewOption("label", "Datetime"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ), form.NewFieldDatetimeLocal("DateTimeLocal"). WithOptions( form.NewOption("label", "DateTime local"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ), form.NewFieldTime("Time"). WithOptions( form.NewOption("label", "Time"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ), @@ -180,7 +180,7 @@ func CreateDataForm() *form.Form { WithOptions( form.NewOption("choices", itemsChoices), form.NewOption("label", "Select"), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ). @@ -192,7 +192,7 @@ func CreateDataForm() *form.Form { form.NewOption("choices", itemsChoices), form.NewOption("label", "Select (expanded)"), form.NewOption("expanded", true), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ), @@ -202,7 +202,7 @@ func CreateDataForm() *form.Form { form.NewOption("choices", itemsChoices), form.NewOption("label", "Multiple select"), form.NewOption("multiple", true), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ). @@ -217,7 +217,7 @@ func CreateDataForm() *form.Form { form.NewOption("label", "Multiple select (expanded)"), form.NewOption("expanded", true), form.NewOption("multiple", true), - form.NewOption("row_attr", map[string]string{ + form.NewOption("row_attr", form.Attrs{ "class": "col-12 mb-3", }), ), @@ -225,14 +225,18 @@ func CreateDataForm() *form.Form { form.NewFieldCsrf("_csrf_token").WithData("my-token"), form.NewSubmit("submit"). WithOptions( - form.NewOption("attr", map[string]string{ + form.NewOption("attr", form.Attrs{ "class": "btn btn-primary", }), ), ). End(). WithOptions( - form.NewOption("attr", map[string]string{ + form.NewOption("help", "form help"), + form.NewOption("help_attr", form.Attrs{ + "class": "btn btn-primary", + }), + form.NewOption("attr", form.Attrs{ "class": "row", }), ). diff --git a/form/field.go b/form/field.go index 5515e81..005c298 100644 --- a/form/field.go +++ b/form/field.go @@ -95,6 +95,13 @@ func NewField(name, widget string) *Field { return data, nil } + f.WithOptions( + NewOption("attr", Attrs{}), + NewOption("row_attr", Attrs{}), + NewOption("label_attr", Attrs{}), + NewOption("help_attr", Attrs{}), + ) + f.Validate = FieldValidation return f diff --git a/form/option.go b/form/option.go index ba483e5..9fccd27 100644 --- a/form/option.go +++ b/form/option.go @@ -1,5 +1,7 @@ package form +import "strings" + // @license GNU AGPL version 3 or any later version // // This program is free software: you can redistribute it and/or modify @@ -35,6 +37,42 @@ func (o *Option) AsString() string { return o.Value.(string) } -func (o *Option) AsMapString() map[string]string { - return o.Value.(map[string]string) +func (o *Option) AsAttrs() Attrs { + return o.Value.(Attrs) +} + +type Attrs map[string]string + +func (a Attrs) Append(name, value string) { + v, ok := a[name] + + if !ok { + v = value + } else { + v = value + " " + v + } + + a[name] = v +} + +func (a Attrs) Prepend(name, value string) { + v, ok := a[name] + + if !ok { + v = value + } else { + v += " " + value + } + + a[name] = v +} + +func (a Attrs) Remove(name, value string) { + v, ok := a[name] + + if !ok { + v = strings.ReplaceAll(v, value, "") + } + + a[name] = v } diff --git a/theme/bootstrap5.go b/theme/bootstrap5.go index 6966955..5c06567 100644 --- a/theme/bootstrap5.go +++ b/theme/bootstrap5.go @@ -1,5 +1,12 @@ package theme +import ( + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/validation" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + // @license GNU AGPL version 3 or any later version // // This program is free software: you can redistribute it and/or modify @@ -18,144 +25,106 @@ package theme var Bootstrap5 = ExtendTheme(Html5, func() map[string]RenderFunc { theme := make(map[string]RenderFunc) + theme["form_help"] = func(parent map[string]RenderFunc, args ...any) Node { + form := args[0].(*form.Form) + + form.GetOption("help_attr").AsAttrs().Append("class", "form-text") + + return parent["base_form_help"](parent, form) + } + + theme["form_widget_help"] = func(parent map[string]RenderFunc, args ...any) Node { + field := args[0].(*form.Field) + + field.GetOption("help_attr").AsAttrs().Append("class", "form-text") + + return parent["base_form_widget_help"](parent, field) + } + + theme["input"] = func(parent map[string]RenderFunc, args ...any) Node { + field := args[0].(*form.Field) + fieldType := field.GetOption("type").AsString() + + var class string + + if fieldType == "checkbox" || fieldType == "radio" { + class = "form-check-input" + } else if fieldType == "range" { + class = "form-range" + } else { + class = "form-control" + } + + field.GetOption("attr").AsAttrs().Append("class", class) + + return parent["base_input"](parent, field) + } + + theme["form_label"] = func(parent map[string]RenderFunc, args ...any) Node { + field := args[0].(*form.Field) + + field.GetOption("label_attr").AsAttrs().Append("class", "form-label") + + return parent["base_form_label"](parent, field) + } + + theme["choice"] = func(parent map[string]RenderFunc, args ...any) Node { + field := args[0].(*form.Field) + + if !field.HasOption("expanded") || !field.GetOption("expanded").AsBool() { + field.GetOption("attr").AsAttrs().Append("class", "form-control") + } + + return parent["base_choice"](parent, field) + } + + theme["choice_expanded_item"] = func(parent map[string]RenderFunc, args ...any) Node { + return Div(Class("form-check"), args[0].(Node)) + } + + theme["textarea"] = func(parent map[string]RenderFunc, args ...any) Node { + field := args[0].(*form.Field) + + field.GetOption("attr").AsAttrs().Append("class", "form-control") + + return parent["base_textarea"](parent, field) + } + + theme["form_row"] = func(parent map[string]RenderFunc, args ...any) Node { + field := args[0].(*form.Field) + + if field.HasOption("type") { + fieldType := field.GetOption("type").AsString() + + if fieldType == "checkbox" || fieldType == "radio" { + field.GetOption("row_attr").AsAttrs().Append("class", "form-check") + } + } + + if field.Widget == "choice" && field.HasOption("expanded") && field.GetOption("expanded").AsBool() { + field.GetOption("label_attr").AsAttrs().Remove("class", "form-label") + field.GetOption("label_attr").AsAttrs().Append("class", "form-check-label") + field.GetOption("attr").AsAttrs().Append("class", "form-check-input") + } + + return parent["base_form_row"](parent, field) + } + + theme["errors"] = func(parent map[string]RenderFunc, args ...any) Node { + errors := args[0].([]validation.Error) + + var result []Node + + for _, v := range errors { + result = append(result, Text(string(v))) + result = append(result, Br()) + } + + return Div( + Class("invalid-feedback d-block"), + Group(result), + ) + } + return theme }) - -// var Bootstrap5 = map[string]string{ -// "form": `
            -// {{- form_error .Form nil -}} -// -// {{- form_help .Form -}} -// -// {{- range $field := .Form.Fields -}} -// {{- form_row $field -}} -// {{- end -}} -//
            `, -// "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`, -// "help": ` -// {{- if gt (len .Help) 0 -}} -//
            {{ .Help }}
            -// {{- end -}} -// `, -// "label": ` -// {{ if .Field.HasOption "label" }} -// {{ $label := (.Field.GetOption "label").Value }} -// -// {{- if ne $label "" -}} -// -// {{- end -}} -// {{- end -}} -// `, -// "input": ` -// {{- $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 -}} -// {{- $class := "form-control" }} -// -// {{- if eq $type.Value "checkbox" -}} -// {{- $value = 1 -}} -// {{- end -}} -// -// {{- if or (eq $type.Value "checkbox") (eq $type.Value "radio") -}} -// {{- $class = "form-check-input" -}} -// {{- end -}} -// -// {{- if eq $type.Value "range" -}} -// {{- $class = "form-range" -}} -// {{- end -}} -// -// {{- if or (eq $type.Value "submit") (eq $type.Value "reset") (eq $type.Value "button") -}} -// {{- $class = "" -}} -// -// {{ if .Field.HasOption "attr" }} -// {{ $class = (.Field.GetOption "attr").Value.attr.class }} -// {{ end }} -// {{- end -}} -// -// -// `, -// "textarea": ` -// -// `, -// "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 -}} -// -// {{- if and (not $required) (not $isMultiple) -}} -// {{- $keyAdd = 1 -}} -// {{- end -}} -// -// {{- if $isExpanded -}} -// {{- if and (not $required) (not $isMultiple) -}} -//
            -// -// -//
            -// {{- end -}} -// -// {{- range $key, $choice := $choices.GetChoices -}} -//
            -// -// -//
            -// {{- end -}} -// {{- else -}} -// -// {{- end -}} -// `, -// "sub_form": ` -//
            -// {{ if .Field.HasOption "label" }} -// {{ $label := (.Field.GetOption "label").Value }} -// -// {{- if ne $label "" -}} -// {{ $label }} -// {{- end -}} -// {{- end -}} -// -// {{ form_widget_help .Field }} -// -// {{- range $field := .Field.Children -}} -// {{- form_row $field -}} -// {{- end -}} -//
            -// `, -// "error": ` -// {{- if gt (len .Errors) 0 -}} -//
            -// {{- range $error := .Errors -}} -//
            {{- $error -}}
            -// {{- end -}} -//
            -// {{- end -}} -// `, -// "row": `
            -// {{ $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_widget .Field -}} -// {{- form_error nil .Field -}} -// -// {{ if and (eq (len .Field.Children) 0) ($labelAfterWidget) }} -// {{- form_label .Field -}} -// {{ end }} -// -// {{- form_widget_help .Field -}} -//
            `, -// } diff --git a/theme/html5.go b/theme/html5.go index a3dc337..a5803a9 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -28,27 +28,27 @@ import ( var Html5 = CreateTheme(func() map[string]RenderFunc { theme := make(map[string]RenderFunc) - theme["attributes"] = func(args ...any) Node { + theme["attributes"] = func(parent map[string]RenderFunc, args ...any) Node { var result []Node - for i, v := range args[0].(map[string]string) { + for i, v := range args[0].(form.Attrs) { result = append(result, Attr(i, v)) } return Group(result) } - theme["form_attributes"] = func(args ...any) Node { + theme["form_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { form := args[0].(*form.Form) if !form.HasOption("attr") { return Raw("") } - return theme["attributes"](form.GetOption("attr").AsMapString()) + return parent["attributes"](parent, form.GetOption("attr").AsAttrs()) } - theme["errors"] = func(args ...any) Node { + theme["errors"] = func(parent map[string]RenderFunc, args ...any) Node { errors := args[0].([]validation.Error) var result []Node @@ -58,73 +58,81 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { } return Ul( - Class("form-errors"), Group(result), ) } - theme["form_errors"] = func(args ...any) Node { + theme["form_errors"] = func(parent map[string]RenderFunc, args ...any) Node { form := args[0].(*form.Form) return If( len(form.Errors) > 0, - theme["errors"](form.Errors), + parent["errors"](parent, form.Errors), ) } - theme["form_widget_errors"] = func(args ...any) Node { + theme["form_widget_errors"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) return If( len(field.Errors) > 0, - theme["errors"](field.Errors), + parent["errors"](parent, field.Errors), ) } - theme["help"] = func(args ...any) Node { + theme["help"] = func(parent map[string]RenderFunc, args ...any) Node { help := args[0].(string) + var extra Node if len(help) == 0 { return Raw("") } + if len(args) == 2 { + extra = args[1].(Node) + } + return Div( - Class("form-help"), - Text("ok"), + Text(help), + extra, ) } - theme["form_help"] = func(args ...any) Node { + theme["form_help"] = func(parent map[string]RenderFunc, args ...any) Node { form := args[0].(*form.Form) if !form.HasOption("help") { return Raw("") } - return theme["help"](form.GetOption("help").AsString()) + return parent["help"]( + parent, + form.GetOption("help").AsString(), + parent["attributes"](parent, form.GetOption("help_attr").AsAttrs()), + ) } - theme["form_widget_help"] = func(args ...any) Node { + theme["form_widget_help"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) if !field.HasOption("help") { return Raw("") } - return theme["help"](field.GetOption("help").AsString()) + return parent["help"]( + parent, + field.GetOption("help").AsString(), + parent["attributes"](parent, field.GetOption("help_attr").AsAttrs()), + ) } - theme["label_attributes"] = func(args ...any) Node { + theme["label_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) - if !field.HasOption("label_attr") { - return Raw("") - } - - return theme["attributes"](field.GetOption("label_attr").AsMapString()) + return parent["attributes"](parent, field.GetOption("label_attr").AsAttrs()) } - theme["form_label"] = func(args ...any) Node { + theme["form_label"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) if !field.HasOption("label") { @@ -134,36 +142,31 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { label := field.GetOption("label").AsString() return If(len(label) > 0, Label( - Class("form-label"), For(field.GetId()), - theme["label_attributes"](field), + parent["label_attributes"](parent, field), Text(label), )) } - theme["field_attributes"] = func(args ...any) Node { + theme["field_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) - if !field.HasOption("attr") { - return Raw("") - } - - return theme["attributes"](field.GetOption("attr").AsMapString()) + return parent["attributes"](parent, field.GetOption("attr").AsAttrs()) } - theme["textarea_attributes"] = func(args ...any) Node { - return theme["field_attributes"](args...) + theme["textarea_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { + return parent["field_attributes"](parent, args...) } - theme["input_attributes"] = func(args ...any) Node { - return theme["field_attributes"](args...) + theme["input_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { + return parent["field_attributes"](parent, args...) } - theme["sub_form_attributes"] = func(args ...any) Node { - return theme["field_attributes"](args...) + theme["sub_form_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { + return parent["field_attributes"](parent, args...) } - theme["input"] = func(args ...any) Node { + theme["input"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) fieldType := "text" @@ -184,11 +187,11 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { Value(value), If(fieldType == "checkbox" && field.Data != false, Checked()), If(field.HasOption("required") && field.GetOption("required").AsBool(), Required()), - theme["input_attributes"](field), + parent["input_attributes"](parent, field), ) } - theme["choice_options"] = func(args ...any) Node { + theme["choice_options"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) choices := field.GetOption("choices").Value.(*form.Choices) @@ -214,7 +217,15 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { return Group(options) } - theme["choice_expanded"] = func(args ...any) Node { + theme["choice_expanded_item"] = func(parent map[string]RenderFunc, args ...any) Node { + return args[0].(Node) + } + + theme["choice_attributes"] = func(parent map[string]RenderFunc, args ...any) Node { + return parent["field_attributes"](parent, args...) + } + + theme["choice_expanded"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) choices := field.GetOption("choices").Value.(*form.Choices) @@ -227,40 +238,48 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { if !isMultiple && !isRequired { id := fmt.Sprintf("%s-%s", field.GetId(), "none") - items = append(items, Group([]Node{ + items = append(items, parent["choice_expanded_item"](parent, Group([]Node{ Input( Name(field.GetName()), ID(id), Value(""), Type("radio"), - theme["input_attributes"](field), + parent["choice_attributes"](parent, field), If(cast.ToString(field.Data) == "", Checked()), ), - Label(For(id), Text(noneLabel)), - })) + Label( + For(id), + Text(noneLabel), + parent["label_attributes"](parent, field), + ), + }))) } for key, choice := range choices.GetChoices() { id := fmt.Sprintf("%s-%d", field.GetId(), key) - items = append(items, Group([]Node{ + items = append(items, parent["choice_expanded_item"](parent, Group([]Node{ Input( Name(field.GetName()), ID(id), Value(choice.Value), If(isMultiple, Type("checkbox")), If(!isMultiple, Type("radio")), - theme["input_attributes"](field), + parent["choice_attributes"](parent, field), If(choices.Match(field, choice.Value), Checked()), ), - Label(For(id), Text(choice.Label)), - })) + Label( + For(id), + Text(choice.Label), + parent["label_attributes"](parent, field), + ), + }))) } return Group(items) } - theme["choice"] = func(args ...any) Node { + theme["choice"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) isRequired := field.HasOption("required") && field.GetOption("required").AsBool() @@ -271,31 +290,32 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { _ = noneLabel if isExpanded { - return theme["choice_expanded"](field) + return parent["choice_expanded"](parent, field) } else { return Select( ID(field.GetId()), If(isRequired, Required()), If(isMultiple, Multiple()), Name(field.GetName()), - theme["choice_options"](field), + parent["choice_attributes"](parent, field), + parent["choice_options"](parent, field), ) } } - theme["textarea"] = func(args ...any) Node { + theme["textarea"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) return Textarea( Name(field.GetName()), ID(field.GetId()), If(field.HasOption("required") && field.GetOption("required").AsBool(), Required()), - theme["textarea_attributes"](field), + parent["textarea_attributes"](parent, field), Text(cast.ToString(field.Data)), ) } - theme["sub_form_label"] = func(args ...any) Node { + theme["sub_form_label"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) if !field.HasOption("label") { @@ -305,88 +325,92 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { label := field.GetOption("label").AsString() return If(len(label) > 0, Legend( - Class("form-label"), - theme["label_attributes"](field), + parent["label_attributes"](parent, field), Text(label), )) } - theme["sub_form_content"] = func(args ...any) Node { + theme["sub_form_content"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) - return theme["form_fields"](field.Children) + return parent["form_fields"](parent, field.Children) } - theme["sub_form"] = func(args ...any) Node { + theme["sub_form"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) return FieldSet( ID(field.GetId()), - theme["sub_form_label"](field), - theme["sub_form_attributes"](field), - theme["sub_form_content"](field), + parent["sub_form_label"](parent, field), + parent["sub_form_attributes"](parent, field), + parent["sub_form_content"](parent, field), ) } - theme["form_widget"] = func(args ...any) Node { + theme["form_widget"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) - tpl, ok := theme[field.Widget] + tpl, ok := parent[field.Widget] if !ok { return Raw("Invalid field widget: " + field.Widget) } - return tpl(field) + return tpl(parent, field) } - theme["form_row"] = func(args ...any) Node { + theme["form_row"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) isCheckbox := field.HasOption("type") && field.GetOption("type").AsString() == "checkbox" hasChildren := len(field.Children) > 0 labelAfter := isCheckbox && !hasChildren - label := theme["form_label"](field) + label := parent["form_label"](parent, field) + attrs := Raw("") + + if field.HasOption("row_attr") { + attrs = parent["attributes"](parent, field.GetOption("row_attr").AsAttrs()) + } return Div( - Class("form-row"), + attrs, If(!labelAfter, label), - theme["form_widget_errors"](field), - theme["form_widget"](field), + parent["form_widget_errors"](parent, field), + parent["form_widget"](parent, field), If(labelAfter, label), - theme["form_widget_help"](field), + parent["form_widget_help"](parent, field), ) } - theme["form_fields"] = func(args ...any) Node { + theme["form_fields"] = func(parent map[string]RenderFunc, args ...any) Node { var items []Node for _, item := range args[0].([]*form.Field) { - items = append(items, theme["form_row"](item)) + items = append(items, parent["form_row"](parent, item)) } return Group(items) } - theme["form_content"] = func(args ...any) Node { + theme["form_content"] = func(parent map[string]RenderFunc, args ...any) Node { form := args[0].(*form.Form) return Div( - theme["form_errors"](form), - theme["form_help"](form), - theme["form_fields"](form.Fields), + parent["form_errors"](parent, form), + parent["form_help"](parent, form), + parent["form_fields"](parent, form.Fields), ) } - theme["form"] = func(args ...any) Node { + theme["form"] = func(parent map[string]RenderFunc, args ...any) Node { form := args[0].(*form.Form) return Form( Action(form.Action), Method(form.Method), - theme["form_attributes"](form), - theme["form_content"](form), + parent["form_attributes"](parent, form), + parent["form_content"](parent, form), ) } diff --git a/theme/renderer.go b/theme/renderer.go index 98cbd51..184b60a 100644 --- a/theme/renderer.go +++ b/theme/renderer.go @@ -23,7 +23,7 @@ import ( "maragu.dev/gomponents" ) -type RenderFunc func(args ...any) gomponents.Node +type RenderFunc func(parent map[string]RenderFunc, args ...any) gomponents.Node type Renderer struct { Theme map[string]RenderFunc @@ -45,7 +45,7 @@ func toTemplateHtml(n gomponents.Node) template.HTML { } func (r *Renderer) RenderForm(form *form.Form) template.HTML { - return toTemplateHtml(r.Theme["form"](form)) + return toTemplateHtml(r.Theme["form"](r.Theme, form)) } func (r *Renderer) FuncMap() template.FuncMap { diff --git a/theme/theme.go b/theme/theme.go index e63864e..9416346 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -8,8 +8,13 @@ func ExtendTheme(base map[string]RenderFunc, generator func() map[string]RenderF extended := CreateTheme(generator) for i, v := range base { - extended[i] = v - extended["base_"+i] = v + _, ok := extended[i] + + if ok { + extended["base_"+i] = v + } else { + extended[i] = v + } } return extended From ba25a987f68c62ed29c95166ad2a032ac14bd773 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Jul 2025 21:32:22 +0200 Subject: [PATCH 041/117] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a746cda..5147ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## [Unreleased] +### Added + +- feat: replace templates with gomponents +- feat: add constraints (isodd, iseven) + ## v1.1.0 ### Added From 9cc9020005eaf91ca9bc5f36bc4612f2f5938e9c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Jul 2025 22:34:12 +0200 Subject: [PATCH 042/117] feat: add go template functions --- CHANGELOG.md | 6 +++++- theme/renderer.go | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5147ac5..75ca8f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,13 @@ ### Added -- feat: replace templates with gomponents +- feat: add go template functions to render a form, a field, etc. - feat: add constraints (isodd, iseven) +### Changed + +- feat: replace templates with gomponents + ## v1.1.0 ### Added diff --git a/theme/renderer.go b/theme/renderer.go index 184b60a..d90b7b7 100644 --- a/theme/renderer.go +++ b/theme/renderer.go @@ -49,9 +49,21 @@ func (r *Renderer) RenderForm(form *form.Form) template.HTML { } func (r *Renderer) FuncMap() template.FuncMap { - return template.FuncMap{ - "form": r.RenderForm, + funcs := template.FuncMap{} + + for _, name := range []string{"form", "form_errors"} { + funcs[name] = func(form *form.Form) template.HTML { + return toTemplateHtml(r.Theme[name](r.Theme, form)) + } } + + for _, name := range []string{"form_row", "form_widget", "form_label", "form_widget_errors"} { + funcs[name] = func(field *form.Field) template.HTML { + return toTemplateHtml(r.Theme[name](r.Theme, field)) + } + } + + return funcs } func (r *Renderer) Render(name, tpl string, args any) template.HTML { From e48e850fa32f96bc5a5deb163071ef10cb508ff4 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 27 Jul 2025 12:34:49 +0200 Subject: [PATCH 043/117] refactor: remove field.f.PrepareView --- form/field.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/form/field.go b/form/field.go index 005c298..7abae57 100644 --- a/form/field.go +++ b/form/field.go @@ -61,7 +61,6 @@ type Field struct { Children []*Field Constraints []validation.Constraint Errors []validation.Error - PrepareView func() map[string]any BeforeMount func(data any) (any, error) BeforeBind func(data any) (any, error) Validate func(f *Field) bool @@ -81,12 +80,6 @@ func NewField(name, widget string) *Field { Data: nil, } - f.PrepareView = func() map[string]any { - m := make(map[string]any) - - return m - } - f.BeforeMount = func(data any) (any, error) { return data, nil } From c41f44bf289a00288e94fec0d164d6f4ffe3e3aa Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 27 Jul 2025 12:43:21 +0200 Subject: [PATCH 044/117] refactor(bootstrap): optimize class customization --- theme/bootstrap5.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/theme/bootstrap5.go b/theme/bootstrap5.go index 5c06567..cc3548c 100644 --- a/theme/bootstrap5.go +++ b/theme/bootstrap5.go @@ -63,7 +63,15 @@ var Bootstrap5 = ExtendTheme(Html5, func() map[string]RenderFunc { theme["form_label"] = func(parent map[string]RenderFunc, args ...any) Node { field := args[0].(*form.Field) - field.GetOption("label_attr").AsAttrs().Append("class", "form-label") + var class string + + if field.Widget == "choice" && field.HasOption("expanded") && field.GetOption("expanded").AsBool() { + class = "form-check-label" + } else { + class = "form-label" + } + + field.GetOption("label_attr").AsAttrs().Append("class", class) return parent["base_form_label"](parent, field) } @@ -101,12 +109,6 @@ var Bootstrap5 = ExtendTheme(Html5, func() map[string]RenderFunc { } } - if field.Widget == "choice" && field.HasOption("expanded") && field.GetOption("expanded").AsBool() { - field.GetOption("label_attr").AsAttrs().Remove("class", "form-label") - field.GetOption("label_attr").AsAttrs().Append("class", "form-check-label") - field.GetOption("attr").AsAttrs().Append("class", "form-check-input") - } - return parent["base_form_row"](parent, field) } From 526f508da7df10f67423aa4f239d3bc311eae7d8 Mon Sep 17 00:00:00 2001 From: CI Date: Sun, 27 Jul 2025 10:55:13 +0000 Subject: [PATCH 045/117] Build doc --- docs/fields/index.html | 14 ++++++++++---- docs/workflow/index.html | 2 +- en.search-data.json | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/fields/index.html b/docs/fields/index.html index fe67b0d..07fa729 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -24,7 +24,7 @@ Choice func NewFieldChoice(name string) *Field Generates inputs (checkbox or rad - +
            myForm.WithOptions(
                 form.NewOption("help", "A help for the form"),
                 // <form data-foo="bar" data-bar="bar" ...
            -    form.NewOption("attr", map[string]string{
            +    form.NewOption("attr", form.Attrs{
                     "data-foo": "foo",
                     "data-bar": "bar",
                 }),
            diff --git a/en.search-data.json b/en.search-data.json
            index b1f9640..abc48d4 100644
            --- a/en.search-data.json
            +++ b/en.search-data.json
            @@ -1 +1 @@
            -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr map[string]string List of extra attributes of the field row_attr map[string]string List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr map[string]string List of extra attributes of the label help string Helper of the field Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", map[string]string{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}}
            \ No newline at end of file
            +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}}
            \ No newline at end of file
            
            From c7172440f13304a85c24a2155a95d401999869f7 Mon Sep 17 00:00:00 2001
            From: CI 
            Date: Sun, 27 Jul 2025 11:14:12 +0000
            Subject: [PATCH 046/117] Build doc
            
            ---
             docs/rendering/index.html | 64 +++++++++++++++++++++++++++++++++------
             docs/rendering/index.xml  |  4 +--
             en.search-data.json       |  2 +-
             3 files changed, 58 insertions(+), 12 deletions(-)
            
            diff --git a/docs/rendering/index.html b/docs/rendering/index.html
            index 5e7ba6c..8423e03 100644
            --- a/docs/rendering/index.html
            +++ b/docs/rendering/index.html
            @@ -10,19 +10,23 @@
             
             
             
            -deblan/go-form
            -  
            +Rendering – deblan/go-form
            +  
             
            -
            +
             
             
             
             
            -  
            -  
            +  
            +  
            +  
               
            -  
            -  
            +  
            +  
             
                 
                 
            @@ -305,8 +309,50 @@
               
            - - +

            Rendering

            +

            go-form allows you to render a form using Go’s built-in template engine. +Here is a simple example that displays a form:

            +
            + +
            http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            +    myForm := form.NewForm(...)
            +
            +    render := theme.NewRenderer(theme.Html5)
            +    tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`
            +		<html>
            +			<head>
            +				<title>My form</title>
            +			</head>
            +			<body>
            +				{{ form .Form }}
            +			</body>
            +		</html>
            +	`)
            +
            +    w.Header().Set("Content-Type", "text/html; charset=utf-8")
            +
            +    tpl.Execute(w, map[string]any{
            +        "Form": myForm,
            +    })
            +}
            + +
            +
            +

            Other helper functions are available to render specific parts of the form:

            +
              +
            • form_errors: displays the form’s global errors
            • +
            • form_row : renders the label, errors, and widget of a field
            • +
            • form_label: renders only the label of a field
            • +
            • form_widget: renders only the widget of a field
            • +
            • form_widget_errors: renders only the errors of a specific field
            • +
            +
            diff --git a/docs/rendering/index.xml b/docs/rendering/index.xml index fb25960..ebc780d 100644 --- a/docs/rendering/index.xml +++ b/docs/rendering/index.xml @@ -1,8 +1,8 @@ - deblan/go-form – + deblan/go-form – Rendering https://deblan.gitnet.page/go-form/docs/rendering/ - Recent content on deblan/go-form + Recent content in Rendering on deblan/go-form Hugo -- gohugo.io en-us diff --git a/en.search-data.json b/en.search-data.json index abc48d4..6d8ae23 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From ea67ce3570568b15d6b77f6e60dad72725737253 Mon Sep 17 00:00:00 2001 From: CI Date: Sun, 27 Jul 2025 11:27:59 +0000 Subject: [PATCH 047/117] Build doc --- docs/constraints/index.html | 76 +++++++++++++++++++++++++++++-------- en.search-data.json | 2 +- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/docs/constraints/index.html b/docs/constraints/index.html index ed948cb..3aac7e3 100644 --- a/docs/constraints/index.html +++ b/docs/constraints/index.html @@ -22,7 +22,7 @@ Import import ( "gitnet.fr/deblan/go-form/validation" ) Constraints Leng - + Regex
          • +
          • + Is even + +
          • +
          • + Is odd + +
          • Custom constraint @@ -473,46 +481,84 @@ Import import ( "gitnet.fr/deblan/go-form/validation" ) Constraints Leng
        +

        Is even +

        Validate that a number is even.

        +
        + +
        validation.NewIsEven()
        + +
        +
        +

        Is odd +

        Validate that a number is odd.

        +
        + +
        validation.NewIsOdd()
        + +
        +

        Custom constraint -

        Use case: you want to validate that the data is an even number.

        +

        Use case: you want to validate that the data equals “example”

        package validation
         
         import (
        -    "strconv"
        +	"reflect"
         
         	v "gitnet.fr/deblan/go-form/validation"
         )
         
         // Define a struct
        -type IsEven struct {
        +type IsExample struct {
         	Message          string
         	TypeErrorMessage string
         }
         
         // Create a factory
        -func NewIsEven() IsEven {
        +func NewIsExample() IsEven {
         	return IsEven{
        -		Message:          "This value is not an even number.",
        +		Message:          "This value does not equal \"example\".",
         		TypeErrorMessage: "This value can not be processed.",
         	}
         }
         
         // Implement the validation
        -func (c IsEven) Validate(data any) []Error {
        +func (c IsExample) Validate(data any) []Error {
         	errors := []Error{}
         
        -    // The constraint should not validate an empty data
        -	if len(v.NewNotBlank().Validate(data)) == 0 {
        -        i, err := strconv.Atoi(data.(string))
        +    // Should not validate blank data
        +    if len(NewNotBlank().Validate(data)) != 0 {
        +		return []Error{}
        +	}
         
        -        if err != nil {
        -            errors = append(errors, Error(c.TypeErrorMessage))
        -        } else if i%2 != 0 {
        -            errors = append(errors, Error(c.Message))
        +	t := reflect.TypeOf(data)
        +
        +	if t.Kind() == reflect.Ptr {
        +		t = t.Elem()
        +	}
        +
        +	switch t.Kind() {
        +	case reflect.String:
        +		if data.(string) != "example" {
        +    		errors = append(errors, Error(c.Message))
                 }
        -    }
        +
        +	default:
        +		errors = append(errors, Error(c.TypeErrorMessage))
        +	}
         
             return errors
         }
        diff --git a/en.search-data.json b/en.search-data.json index 6d8ae23..08ffca2 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data is an even number.\npackage validation import ( \"strconv\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsEven struct { Message string TypeErrorMessage string } // Create a factory func NewIsEven() IsEven { return IsEven{ Message: \"This value is not an even number.\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsEven) Validate(data any) []Error { errors := []Error{} // The constraint should not validate an empty data if len(v.NewNotBlank().Validate(data)) == 0 { i, err := strconv.Atoi(data.(string)) if err != nil { errors = append(errors, Error(c.TypeErrorMessage)) } else if i%2 != 0 { errors = append(errors, Error(c.Message)) } } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From 02664be12d98838b77d6b7d3f6d7aef552049654 Mon Sep 17 00:00:00 2001 From: CI Date: Sun, 27 Jul 2025 11:29:27 +0000 Subject: [PATCH 048/117] Build doc --- docs/constraints/index.html | 2 +- en.search-data.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/constraints/index.html b/docs/constraints/index.html index 3aac7e3..6ddf9cd 100644 --- a/docs/constraints/index.html +++ b/docs/constraints/index.html @@ -540,7 +540,7 @@ Import import ( "gitnet.fr/deblan/go-form/validation" ) Constraints Leng errors := []Error{} // Should not validate blank data - if len(NewNotBlank().Validate(data)) != 0 { + if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } diff --git a/en.search-data.json b/en.search-data.json index 08ffca2..02039f5 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From c30aa832a2d3d4f7269dc53900e8313e1fc05e4c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 27 Jul 2025 13:33:44 +0200 Subject: [PATCH 049/117] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ca8f8..dd0fae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [Unreleased] +## v1.2.0 + ### Added - feat: add go template functions to render a form, a field, etc. From e108422b04f2b3c09f79e1f11d2bbc53346fde7b Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 27 Jul 2025 13:34:47 +0200 Subject: [PATCH 050/117] update changelog --- CHANGELOG.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0fae0..223ea81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,17 @@ ## [Unreleased] -## v1.2.0 - -### Added - -- feat: add go template functions to render a form, a field, etc. -- feat: add constraints (isodd, iseven) - -### Changed - -- feat: replace templates with gomponents - ## v1.1.0 ### Added +- feat: add go template functions to render a form, a field, etc. - feat: add constraints (isodd, iseven) - refactor: refactor constraint (not blank check) +### Changed + +- feat: replace templates with gomponents + ## v1.0.1 ### Added From 886708b3377e365cd8c9f05b90c3423941aca527 Mon Sep 17 00:00:00 2001 From: CI Date: Sun, 27 Jul 2025 11:43:39 +0000 Subject: [PATCH 051/117] Build doc --- docs/index.xml | 10 +++++++-- docs/rendering/index.xml | 10 +++++++-- docs/rendering/theming/index.html | 35 ++++++++++++++++++++++--------- en.search-data.json | 2 +- sitemap.xml | 4 ++-- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/docs/index.xml b/docs/index.xml index 52dc1d8..cfdff96 100644 --- a/docs/index.xml +++ b/docs/index.xml @@ -40,7 +40,7 @@ - + Theming https://deblan.gitnet.page/go-form/docs/rendering/theming/ Mon, 01 Jan 0001 00:00:00 +0000 @@ -48,7 +48,13 @@ - + <p><strong>go-form</strong> provides 2 themes:</p> +<ul> +<li><code>theme.Html5</code>: a basic view without classes</li> +<li><code>theme.Bootstrap5</code>: a theme for <a href="https://getbootstrap.com/" target="_blank" rel="noopener">Bootstrap 5</a></li> +</ul> +<p>You can add a custom theme. Learn by reading the <a href="https://gitnet.fr/deblan/go-form/src/branch/main/theme/bootstrap5.go" target="_blank" rel="noopener">Bootstrap5</a> theme.</p> + diff --git a/docs/rendering/index.xml b/docs/rendering/index.xml index ebc780d..1a42779 100644 --- a/docs/rendering/index.xml +++ b/docs/rendering/index.xml @@ -15,7 +15,7 @@ - + Theming https://deblan.gitnet.page/go-form/docs/rendering/theming/ Mon, 01 Jan 0001 00:00:00 +0000 @@ -23,7 +23,13 @@ - + <p><strong>go-form</strong> provides 2 themes:</p> +<ul> +<li><code>theme.Html5</code>: a basic view without classes</li> +<li><code>theme.Bootstrap5</code>: a theme for <a href="https://getbootstrap.com/" target="_blank" rel="noopener">Bootstrap 5</a></li> +</ul> +<p>You can add a custom theme. Learn by reading the <a href="https://gitnet.fr/deblan/go-form/src/branch/main/theme/bootstrap5.go" target="_blank" rel="noopener">Bootstrap5</a> theme.</p> + diff --git a/docs/rendering/theming/index.html b/docs/rendering/theming/index.html index 754fa26..847726a 100644 --- a/docs/rendering/theming/index.html +++ b/docs/rendering/theming/index.html @@ -10,21 +10,30 @@ -deblan/go-form - +Theming – deblan/go-form + - - + + - - + + + - - + + @@ -310,8 +319,14 @@
        - - +

        Theming

        +

        go-form provides 2 themes:

        +
          +
        • theme.Html5: a basic view without classes
        • +
        • theme.Bootstrap5: a theme for Bootstrap 5
        • +
        +

        You can add a custom theme. Learn by reading the Bootstrap5 theme.

        +
        diff --git a/en.search-data.json b/en.search-data.json index 02039f5..9606dc2 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 33d3a6e..509675b 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -13,14 +13,14 @@ https://deblan.gitnet.page/go-form/docs/constraints/ https://deblan.gitnet.page/go-form/docs/rendering/ - - https://deblan.gitnet.page/go-form/docs/rendering/theming/ https://deblan.gitnet.page/go-form/categories/ https://deblan.gitnet.page/go-form/docs/ https://deblan.gitnet.page/go-form/tags/ + + https://deblan.gitnet.page/go-form/docs/rendering/theming/ https://deblan.gitnet.page/go-form/ From eb85de0376c77e0e3592033cc83abdd877bf3c00 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 27 Jul 2025 14:32:54 +0200 Subject: [PATCH 052/117] feat(form): add default options --- form/form.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/form/form.go b/form/form.go index b943860..2b20bae 100644 --- a/form/form.go +++ b/form/form.go @@ -42,6 +42,10 @@ func NewForm(fields ...*Field) *Form { f.Method = "POST" f.Name = "form" f.Add(fields...) + f.WithOptions( + NewOption("attr", Attrs{}), + NewOption("help_attr", Attrs{}), + ) return f } From 33e3a97d07ebcf15924fafafbaf9a14f70d88cf6 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 27 Jul 2025 14:33:18 +0200 Subject: [PATCH 053/117] fix(choice): fix Match when field data is nil --- form/field_choice.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/form/field_choice.go b/form/field_choice.go index e8f735e..6a4014d 100644 --- a/form/field_choice.go +++ b/form/field_choice.go @@ -38,6 +38,10 @@ type Choices struct { } func (c *Choices) Match(f *Field, value string) bool { + if f.Data == nil { + return false + } + if f.IsSlice { v := reflect.ValueOf(f.Data) From 32c383e758afbd759197061ccffab6788f96d383 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 27 Jul 2025 14:33:52 +0200 Subject: [PATCH 054/117] update changelog --- CHANGELOG.md | 7 +++++++ main.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 223ea81..195158a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## [Unreleased] +## v1.1.1 + +### Fixed + +- feat(form): add default options +- fix(choice): fix Match when field data is nil + ## v1.1.0 ### Added diff --git a/main.go b/main.go index 55e9229..a08940d 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "html/template" "log" "net/http" + "os" "github.com/yassinebenaid/godump" "gitnet.fr/deblan/go-form/example" @@ -169,6 +170,8 @@ func main() { "Form": f, "Dump": template.HTML(dump.Sprint(data)), }) + + os.Stdout }) log.Fatal(http.ListenAndServe(":1122", nil)) From a36d1f31e09c922b2ce83454e87ad84dfd2620ca Mon Sep 17 00:00:00 2001 From: CI Date: Sun, 27 Jul 2025 13:21:40 +0000 Subject: [PATCH 055/117] Build doc --- docs/fields/index.html | 2011 +++++++++++++++++++++++++++++++++++++++- en.search-data.json | 2 +- 2 files changed, 1989 insertions(+), 24 deletions(-) diff --git a/docs/fields/index.html b/docs/fields/index.html index 07fa729..21a6824 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -12,8 +12,7 @@ Fields – deblan/go-form +Checkbox DocExample func NewFieldCheckbox(name string) *Field import ( "fmt" "html/template" "strings" "gitnet.fr/deblan/go-form/form" "gitnet.fr/deblan/go-form/theme" ) func main() { field := form.NewFieldCheckbox("Foo") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{"Form": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]" /> @@ -22,14 +21,12 @@ Choice func NewFieldChoice(name string) *Field Generates inputs (checkbox or rad - +Checkbox DocExample func NewFieldCheckbox(name string) *Field import ( "fmt" "html/template" "strings" "gitnet.fr/deblan/go-form/form" "gitnet.fr/deblan/go-form/theme" ) func main() { field := form.NewFieldCheckbox("Foo") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{"Form": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]"> + +Checkbox DocExample func NewFieldCheckbox(name string) *Field import ( "fmt" "html/template" "strings" "gitnet.fr/deblan/go-form/form" "gitnet.fr/deblan/go-form/theme" ) func main() { field := form.NewFieldCheckbox("Foo") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{"Form": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]"> @@ -469,7 +466,22 @@ Choice func NewFieldChoice(name string) *Field Generates inputs (checkbox or rad

        Fields

        A field represents a field in a form.

        Checkbox -

        +
        +
        +
        +
        +
        func NewFieldCheckbox(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldCheckbox("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=checkbox]

        Choice -

        +
        +
        +
        +
        +
        func NewFieldChoice(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldChoice("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates inputs (checkbox or radio) or selects

        Csrf -

        +
        +
        +
        +
        +
        func NewFieldCsrf(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldCsrf("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Date -

        +
        +
        +
        +
        +
        func NewFieldDate(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldDate("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=date] with default transformers

        Datetime -

        +
        +
        +
        +
        +
        func NewFieldDatetime(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldDatetime("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=datetime] with default transformers

        DatetimeLocal -

        +
        +
        +
        +
        +
        func NewFieldDatetimeLocal(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldDatetimeLocal("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=datetime-local] with default transformers

        Hidden -

        +
        +
        +
        +
        +
        func NewFieldHidden(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldHidden("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=hidden]

        Mail -

        +
        +
        +
        +
        +
        func NewFieldMail(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldMail("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=email]

        Number -

        +
        +
        +
        +
        +
        func NewFieldNumber(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldNumber("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=number] with default transformers

        Password -

        +
        +
        +
        +
        +
        func NewFieldPassword(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldPassword("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=password]

        Range -

        +
        +
        +
        +
        +
        func NewFieldRange(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldRange("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=range]

        Sub Form -

        +
        +
        +
        +
        +
        func NewFieldSubForm(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldSubForm("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Alias:

        @@ -649,7 +2122,22 @@ Choice func NewFieldChoice(name string) *Field Generates inputs (checkbox or rad

        Generates a sub form

        Text -

        +
        +
        +
        +
        +
        func NewFieldText(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldText("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=text]

        Textarea -

        +
        +
        +
        +
        +
        func NewFieldTextarea(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldTextarea("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates a textarea

        Time -

        +
        +
        +
        +
        +
        func NewFieldTime(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewFieldTime("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=time] with default transformers

        Submit -

        +
        +
        +
        +
        +
        func NewSubmit(name string) *Field
        +
        + +
        +
        import (
        +    "fmt"
        +    "html/template"
        +    "strings"
        +
        +    "gitnet.fr/deblan/go-form/form"
        +    "gitnet.fr/deblan/go-form/theme"
        +)
        +
        +func main() {
        +    field := form.NewSubmit("Foo")
        +
        +    r(form.NewForm(field))
        +}
        +
        +func r(f *form.Form) {
        +    render := theme.NewRenderer(theme.Html5)
        +    tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse()
        +    b := new(strings.Builder)
        +    tpl.Execute(b, map[string]any{"Form": f})
        +    fmt.Println(b.String())
        +}
        + +
        +
        +
        +
        + + + +
        + + +
        +

        Generates an input[type=submit]

        Methods

        Add diff --git a/en.search-data.json b/en.search-data.json index 9606dc2..6b199b3 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field Date func NewFieldDate(name string) *Field Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field Generates an input[type=password]\nRange func NewFieldRange(name string) *Field Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field Generates a textarea\nTime func NewFieldTime(name string) *Field Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox DocExample func NewFieldCheckbox(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldCheckbox(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice DocExample func NewFieldChoice(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldChoice(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf DocExample func NewFieldCsrf(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldCsrf(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Date DocExample func NewFieldDate(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldDate(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime DocExample func NewFieldDatetime(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldDatetime(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal DocExample func NewFieldDatetimeLocal(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldDatetimeLocal(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden DocExample func NewFieldHidden(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldHidden(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail DocExample func NewFieldMail(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldMail(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber DocExample func NewFieldNumber(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldNumber(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword DocExample func NewFieldPassword(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldPassword(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange DocExample func NewFieldRange(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldRange(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form DocExample func NewFieldSubForm(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldSubForm(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText DocExample func NewFieldText(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldText(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea DocExample func NewFieldTextarea(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldTextarea(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates a textarea\nTime DocExample func NewFieldTime(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewFieldTime(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit DocExample func NewSubmit(name string) *Field import ( \"fmt\" \"html/template\" \"strings\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) func main() { field := form.NewSubmit(\"Foo\") r(form.NewForm(field)) } func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse() b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) } Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\") tpl.Execute(w, map[string]any{ \"Form\": myForm, }) } Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From 28bfc3026cae84621ae5c77ccc48a69f5dade927 Mon Sep 17 00:00:00 2001 From: CI Date: Sun, 27 Jul 2025 13:38:40 +0000 Subject: [PATCH 056/117] Build doc --- docs/fields/index.html | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/fields/index.html b/docs/fields/index.html index 21a6824..f7d1eee 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -571,7 +571,7 @@ Checkbox DocExample func NewFieldCheckbox(name string) *Field import ( "fmt& -

        -

        Date -

        -
        -
        -
        -
        +
        func NewFieldDate(name string) *Field
        -
        +
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates inputs (checkbox or radio) or selects

        Csrf @@ -792,143 +564,29 @@ func r(f *form.Form) { field := form.NewFieldCsrf("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Date

        @@ -947,143 +605,29 @@ func r(f *form.Form) { field := form.NewFieldDate("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=date] with default transformers

        Datetime @@ -1103,143 +647,29 @@ func r(f *form.Form) { field := form.NewFieldDatetime("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=datetime] with default transformers

        DatetimeLocal @@ -1259,143 +689,29 @@ func r(f *form.Form) { field := form.NewFieldDatetimeLocal("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=datetime-local] with default transformers

        Hidden @@ -1415,143 +731,29 @@ func r(f *form.Form) { field := form.NewFieldHidden("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=hidden]

        Mail @@ -1571,143 +773,29 @@ func r(f *form.Form) { field := form.NewFieldMail("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=email]

        Number @@ -1727,143 +815,29 @@ func r(f *form.Form) { field := form.NewFieldNumber("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=number] with default transformers

        Password @@ -1883,143 +857,29 @@ func r(f *form.Form) { field := form.NewFieldPassword("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=password]

        Range @@ -2039,143 +899,29 @@ func r(f *form.Form) { field := form.NewFieldRange("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=range]

        Sub Form @@ -2195,143 +941,29 @@ func r(f *form.Form) { field := form.NewFieldSubForm("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Alias:

        @@ -2364,143 +996,29 @@ func r(f *form.Form) { field := form.NewFieldText("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=text]

        Textarea @@ -2520,143 +1038,29 @@ func r(f *form.Form) { field := form.NewFieldTextarea("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates a textarea

        Time @@ -2676,143 +1080,29 @@ func r(f *form.Form) { field := form.NewFieldTime("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=time] with default transformers

        Submit @@ -2832,143 +1122,29 @@ func r(f *form.Form) { field := form.NewSubmit("Foo")
        -Test me - -
        -
        - - - -
        + Test me + +
        +
        + + + +
        -

        Generates an input[type=submit]

        Methods @@ -3276,7 +1452,8 @@ func r(f *form.Form) {
        + > +

        diff --git a/docs/form/index.html b/docs/form/index.html index 5ccf04c..b658230 100644 --- a/docs/form/index.html +++ b/docs/form/index.html @@ -30,7 +30,7 @@ - + +

        diff --git a/docs/index.html b/docs/index.html index 1b59815..cf2c7f4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -36,7 +36,7 @@ A form builder based on fields declarations and independent of structs Validatio - + +
        diff --git a/docs/installation/index.html b/docs/installation/index.html index 41530da..5a6122f 100644 --- a/docs/installation/index.html +++ b/docs/installation/index.html @@ -40,7 +40,7 @@ go get gitnet.fr/deblan/go-form - + +
        diff --git a/docs/rendering/index.html b/docs/rendering/index.html index 31f6a0b..bb84082 100644 --- a/docs/rendering/index.html +++ b/docs/rendering/index.html @@ -12,7 +12,7 @@ Rendering – deblan/go-form +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "net/http" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/form" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:" /> @@ -21,19 +21,19 @@ myForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := t - +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "net/http" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/form" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> + +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "net/http" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/form" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> - +

        Other helper functions are available to render specific parts of the form:

        • form_errors: displays the form’s global errors
        • @@ -366,7 +410,8 @@ Here is a simple example that displays a form:

          + > +
        diff --git a/docs/rendering/theming/index.html b/docs/rendering/theming/index.html index 847726a..14af2e6 100644 --- a/docs/rendering/theming/index.html +++ b/docs/rendering/theming/index.html @@ -40,7 +40,7 @@ theme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstra - + +
        diff --git a/docs/workflow/index.html b/docs/workflow/index.html index 08f1b71..ead0a36 100644 --- a/docs/workflow/index.html +++ b/docs/workflow/index.html @@ -30,7 +30,7 @@ - + +
        diff --git a/en.search-data.json b/en.search-data.json index bfea287..6530169 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Html5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) tpl, _ := template.New(\"example\").Funcs(render.FuncMap()).Parse(\"{{ form_widget .Form }}\") b := new(strings.Builder) tpl.Execute(b, map[string]any{\"Form\": f}) fmt.Println(b.String()) Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"html/template\" @import \"net/http\" @import @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/form\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file diff --git a/index.html b/index.html index e59c310..3cf9594 100644 --- a/index.html +++ b/index.html @@ -31,7 +31,7 @@ - + +
        diff --git a/js/custom.js b/js/custom.js new file mode 100644 index 0000000..3b7f770 --- /dev/null +++ b/js/custom.js @@ -0,0 +1,126 @@ +function normalizeCode(code) { + return code + .trim() + .replace(/^```go/, '') + .replace(/^```/, '') + .replace(/```$/, '') + .replace(/<([^>]+)>/g, '') + .trim() +} + +function updatePlaygroundView(goplay, id, code) { + const textarea = document.getElementById(`textarea-${id}`) + + textarea.value = code + textarea.style.height = textarea.scrollHeight + 'px'; + textarea.style.overflowY = 'hidden'; + + textarea.addEventListener('input', function() { + this.style.height = 'auto'; + this.style.height = this.scrollHeight + 'px'; + }) + + const toolRun = document.getElementById(`hugo-goplay-tool-${id}-run`) + const toolTry = document.getElementById(`hugo-goplay-tool-${id}-try`) + const toolShare = document.getElementById(`hugo-goplay-tool-${id}-share`) + + toolRun.addEventListener('click', () => { + const parent = document.getElementById(id) + const pre = document.createElement('pre') + const container = document.createElement('code') + + container.classList.add('text') + pre.appendChild(container) + parent.replaceChildren(pre) + + goplay.renderCompile(container, textarea.value); + }); + + [toolTry, toolShare].forEach((v) => { + v.addEventListener('click', async () => { + const shareUrl = await goplay.share(code) + window.open(shareUrl, '_blank').focus(); + }) + }); +} + +function createFieldPlayground(goplay, id, code) { + code = createFieldPlaygroundCode(code) + + updatePlaygroundView(goplay, id, code) +} + +function createPlaygroundWithAutoImportMail(goplay, id, code) { + let lines = normalizeCode(code).split("\n") + let results = ["package main", ""] + let imports = [] + let body = [] + + lines.forEach((line) => { + if (line.substr(0, 7) === '@import') { + imports.push(' ' + line.replace('@import', '').trim()) + } else { + body.push(" " + line) + } + }) + + if (imports.length > 0) { + results.push("import (") + + imports.forEach((v) => { + results.push(v) + }) + + results.push(")", "") + } + + results.push("func main() {") + + body.forEach((v, i) => { + if (i == 0 && v.trim() == "") { + return + } + + results.push(v) + }) + + results.push("}", "") + + updatePlaygroundView(goplay, id, results.join("\n")) +} + +function createFieldPlaygroundCode(code) { + code = normalizeCode(code) + let lines = code.split("\n"); + + for (let i in lines) { + lines[i] = [' ', lines[i]].join('') + } + + code = lines.join("\n"); + + return `package main + +import ( + 'fmt' + 'html/template' + 'strings' + + 'gitnet.fr/deblan/go-form/form' + 'gitnet.fr/deblan/go-form/theme' +) + +func main() { +${code} + + r(form.NewForm(field)) +} + +func r(f *form.Form) { + render := theme.NewRenderer(theme.Html5) + tpl, _ := template.New('example').Funcs(render.FuncMap()).Parse(\`{{ form_widget (.Form.GetField "Foo") }}\`) + b := new(strings.Builder) + tpl.Execute(b, map[string]any{"Form": f}) + fmt.Println(b.String()) +}` +} diff --git a/tags/index.html b/tags/index.html index a421c6f..494f437 100644 --- a/tags/index.html +++ b/tags/index.html @@ -29,7 +29,7 @@ - + +
        From bffb4ddb1d6f1cba0ac1577beba6a234c4f8ca61 Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 28 Jul 2025 16:15:20 +0000 Subject: [PATCH 071/117] Build doc --- docs/rendering/index.html | 25 ++++++++++++------------- en.search-data.json | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/rendering/index.html b/docs/rendering/index.html index bb84082..2532934 100644 --- a/docs/rendering/index.html +++ b/docs/rendering/index.html @@ -12,7 +12,7 @@ Rendering – deblan/go-form +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "net/http" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": form, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:" /> @@ -21,12 +21,12 @@ myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ - +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "net/http" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": form, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> + +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "net/http" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": form, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> @@ -350,7 +350,6 @@ Here is a simple example that displays a form:

        @import "net/http" @import @import "gitnet.fr/deblan/go-form/example" -@import "gitnet.fr/deblan/go-form/form" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() @@ -361,23 +360,23 @@ tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ - "Form": myForm, + "Form": form, }) fmt.Println(b.String())
        Test me - -
        + +
        - - -
        @@ -387,8 +386,8 @@ fmt.Println(b.String()) createPlaygroundWithAutoImportMail( new GoPlayProxy("https://gp.deblan.gitnet.page"), - "6e91b0a04c7db1b4725aa8ee45865a26", - "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022html\/template\u0022\n@import \u0022net\/http\u0022\n@import\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/form\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nb := new(strings.Builder)\n\ntpl.Execute(w, map[string]any{\n \u0022Form\u0022: myForm,\n})\n\nfmt.Println(b.String())\n\u003c\/pre\u003e\n" + "0545de09fa9a187cbd071b6c38689a26", + "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022html\/template\u0022\n@import \u0022net\/http\u0022\n@import\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nb := new(strings.Builder)\n\ntpl.Execute(w, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(b.String())\n\u003c\/pre\u003e\n" )

        Other helper functions are available to render specific parts of the form:

        diff --git a/en.search-data.json b/en.search-data.json index 6530169..52d74fb 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"html/template\" @import \"net/http\" @import @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/form\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"html/template\" @import \"net/http\" @import @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": form, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From 264916bbb3f77f0aaf10582278421396cad5c8d1 Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 28 Jul 2025 16:16:34 +0000 Subject: [PATCH 072/117] Build doc --- docs/rendering/index.html | 28 ++++++++++++++-------------- en.search-data.json | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/rendering/index.html b/docs/rendering/index.html index 2532934..30d654e 100644 --- a/docs/rendering/index.html +++ b/docs/rendering/index.html @@ -12,7 +12,7 @@ Rendering – deblan/go-form +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "strings" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:" /> @@ -21,12 +21,12 @@ myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "strings" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "html/template" @import "strings" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> @@ -347,7 +347,7 @@ Here is a simple example that displays a form:

        Test me - -
        + +
        - - -
        @@ -386,8 +386,8 @@ fmt.Println(b.String()) createPlaygroundWithAutoImportMail( new GoPlayProxy("https://gp.deblan.gitnet.page"), - "0545de09fa9a187cbd071b6c38689a26", - "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022html\/template\u0022\n@import \u0022net\/http\u0022\n@import\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nb := new(strings.Builder)\n\ntpl.Execute(w, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(b.String())\n\u003c\/pre\u003e\n" + "b7b61b017c775b00ff044d2855132d68", + "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022html\/template\u0022\n@import \u0022strings\u0022\n@import\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nbuff := new(strings.Builder)\n\ntpl.Execute(buff, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(buff.String())\n\u003c\/pre\u003e\n" )

        Other helper functions are available to render specific parts of the form:

        diff --git a/en.search-data.json b/en.search-data.json index 52d74fb..efbd0fd 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"html/template\" @import \"net/http\" @import @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": form, }) fmt.Println(b.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"html/template\" @import \"strings\" @import @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ \"Form\": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From 884bee9c6d55cdcd60169d2a002ff322fe664373 Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 28 Jul 2025 16:17:24 +0000 Subject: [PATCH 073/117] Build doc --- docs/rendering/index.html | 23 ++++++++++++----------- en.search-data.json | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/rendering/index.html b/docs/rendering/index.html index 30d654e..a4d4567 100644 --- a/docs/rendering/index.html +++ b/docs/rendering/index.html @@ -12,7 +12,7 @@ Rendering – deblan/go-form +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:" /> @@ -21,12 +21,12 @@ myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ - +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> + +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> @@ -346,6 +346,7 @@ Here is a simple example that displays a form:

        Test me - -
        + +
        - - -
        @@ -386,8 +387,8 @@ fmt.Println(buff.String()) createPlaygroundWithAutoImportMail( new GoPlayProxy("https://gp.deblan.gitnet.page"), - "b7b61b017c775b00ff044d2855132d68", - "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022html\/template\u0022\n@import \u0022strings\u0022\n@import\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nbuff := new(strings.Builder)\n\ntpl.Execute(buff, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(buff.String())\n\u003c\/pre\u003e\n" + "0f80eacd59d04956ad1e7f144eff8b7e", + "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022fmt\u0022\n@import \u0022html\/template\u0022\n@import \u0022strings\u0022\n@import\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nbuff := new(strings.Builder)\n\ntpl.Execute(buff, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(buff.String())\n\u003c\/pre\u003e\n" )

        Other helper functions are available to render specific parts of the form:

        diff --git a/en.search-data.json b/en.search-data.json index efbd0fd..4f6deff 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"html/template\" @import \"strings\" @import @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ \"Form\": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"fmt\" @import \"html/template\" @import \"strings\" @import @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ \"Form\": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From 6f6a534e1ad1df10ac6e881e0cdde59a06a9c4aa Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 28 Jul 2025 19:46:13 +0000 Subject: [PATCH 074/117] Build doc --- docs/rendering/index.html | 25 +++++++++++++------------ en.search-data.json | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/rendering/index.html b/docs/rendering/index.html index a4d4567..381bf3a 100644 --- a/docs/rendering/index.html +++ b/docs/rendering/index.html @@ -12,7 +12,7 @@ Rendering – deblan/go-form +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "github.com/yosssi/gohtml" @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:" /> @@ -21,12 +21,12 @@ myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ - +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "github.com/yosssi/gohtml" @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> + +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "github.com/yosssi/gohtml" @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> @@ -350,6 +350,7 @@ Here is a simple example that displays a form:

        @import "html/template" @import "strings" @import +@import "github.com/yosssi/gohtml" @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" @@ -364,20 +365,20 @@ tpl.Execute(buff, map[string]any{ "Form": form, }) -fmt.Println(buff.String()) +fmt.Println(gohtml.Format(buff.String()))
        Test me - -
        + +
        - - -
        @@ -387,8 +388,8 @@ fmt.Println(buff.String()) createPlaygroundWithAutoImportMail( new GoPlayProxy("https://gp.deblan.gitnet.page"), - "0f80eacd59d04956ad1e7f144eff8b7e", - "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022fmt\u0022\n@import \u0022html\/template\u0022\n@import \u0022strings\u0022\n@import\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nbuff := new(strings.Builder)\n\ntpl.Execute(buff, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(buff.String())\n\u003c\/pre\u003e\n" + "d82b883c721a51b71fe97bbd6331c984", + "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022fmt\u0022\n@import \u0022html\/template\u0022\n@import \u0022strings\u0022\n@import\n@import \u0022github.com\/yosssi\/gohtml\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nbuff := new(strings.Builder)\n\ntpl.Execute(buff, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(gohtml.Format(buff.String()))\n\u003c\/pre\u003e\n" )

        Other helper functions are available to render specific parts of the form:

        diff --git a/en.search-data.json b/en.search-data.json index 4f6deff..6b4e646 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"fmt\" @import \"html/template\" @import \"strings\" @import @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ \"Form\": form, }) fmt.Println(buff.String()) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"fmt\" @import \"html/template\" @import \"strings\" @import @import \"github.com/yosssi/gohtml\" @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ \"Form\": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From 17ecc007f6937f25212c16cbfd8bbb700ad40be7 Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 28 Jul 2025 19:48:37 +0000 Subject: [PATCH 075/117] Build doc --- docs/rendering/index.html | 25 +++++++++++++------------ en.search-data.json | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/rendering/index.html b/docs/rendering/index.html index 381bf3a..3dee2cf 100644 --- a/docs/rendering/index.html +++ b/docs/rendering/index.html @@ -12,7 +12,7 @@ Rendering – deblan/go-form +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "github.com/yosssi/gohtml" @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Html5) // render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:" /> @@ -21,12 +21,12 @@ myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ - +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "github.com/yosssi/gohtml" @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Html5) // render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> + +myForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(` <html> <head> <title>My form</title> </head> <body> {{ form .Form }} </body> </html> `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ "Form": myForm, }) fmt.Println(b.String()) @import "fmt" @import "html/template" @import "strings" @import @import "github.com/yosssi/gohtml" @import "gitnet.fr/deblan/go-form/example" @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() render := theme.NewRenderer(theme.Html5) // render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ "Form": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:"> @@ -355,7 +355,8 @@ Here is a simple example that displays a form:

        @import "gitnet.fr/deblan/go-form/theme" form := example.CreateDataForm() -render := theme.NewRenderer(theme.Bootstrap5) +render := theme.NewRenderer(theme.Html5) +// render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New("page").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) @@ -369,16 +370,16 @@ fmt.Println(gohtml.Format(buff.String()))
        Test me - -
        + +
        - - -
        @@ -388,8 +389,8 @@ fmt.Println(gohtml.Format(buff.String())) createPlaygroundWithAutoImportMail( new GoPlayProxy("https://gp.deblan.gitnet.page"), - "d82b883c721a51b71fe97bbd6331c984", - "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022fmt\u0022\n@import \u0022html\/template\u0022\n@import \u0022strings\u0022\n@import\n@import \u0022github.com\/yosssi\/gohtml\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nbuff := new(strings.Builder)\n\ntpl.Execute(buff, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(gohtml.Format(buff.String()))\n\u003c\/pre\u003e\n" + "b4c457542ee01705b336b3d98eb31f2f", + "\n\u003cpre class=\u0022hidden\u0022\u003e\n@import \u0022fmt\u0022\n@import \u0022html\/template\u0022\n@import \u0022strings\u0022\n@import\n@import \u0022github.com\/yosssi\/gohtml\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/example\u0022\n@import \u0022gitnet.fr\/deblan\/go-form\/theme\u0022\n\nform := example.CreateDataForm()\nrender := theme.NewRenderer(theme.Html5)\n\/\/ render := theme.NewRenderer(theme.Bootstrap5)\n\ntpl, _ := template.New(\u0022page\u0022).Funcs(render.FuncMap()).Parse(\u0060{{ form .Form }}\u0060)\n\nbuff := new(strings.Builder)\n\ntpl.Execute(buff, map[string]any{\n \u0022Form\u0022: form,\n})\n\nfmt.Println(gohtml.Format(buff.String()))\n\u003c\/pre\u003e\n" )

        Other helper functions are available to render specific parts of the form:

        diff --git a/en.search-data.json b/en.search-data.json index 6b4e646..e62a58b 100644 --- a/en.search-data.json +++ b/en.search-data.json @@ -1 +1 @@ -{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"fmt\" @import \"html/template\" @import \"strings\" @import @import \"github.com/yosssi/gohtml\" @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ \"Form\": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file +{"/go-form/docs/":{"data":{"":"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 includes a powerful form feature that provides all these features.\ngo-form is heavily influenced by Symfony Form. It includes:\nA form builder based on fields declarations and independent of structs Validation based on constraints Data mounting to populate a form from a struct instance Data binding to populate a struct instance from a submitted form Form renderer with customizable themes "},"title":"Documentation"},"/go-form/docs/constraints/":{"data":{"":"The validation is designed to validate data against constraints.","constraints#Constraints":"Length Validate the length of an array, a slice or a string\nc := validation.NewLength() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(50).WithMax(50) c.WithExact(50) Mail validation.NewMail() Not blank validation.NewNotBlank() Range Validate a number\nc := validation.NewRange() // Define minimum c.WithMin(1) // Define minimum c.WithMax(100) // Define min and max // Equivalent to c.WithMin(1).WithMax(100) c.WithRange(1, 100) Regex Validate a string with a regex\nc := validation.NewRegex(`expression`) // The value must match c.MustMatch() // The value must not match c.MustNotMatch() Is even Validate that a number is even.\nvalidation.NewIsEven() Is odd Validate that a number is odd.\nvalidation.NewIsOdd() ","custom-constraint#Custom constraint":"Use case: you want to validate that the data equals “example”\npackage validation import ( \"reflect\" v \"gitnet.fr/deblan/go-form/validation\" ) // Define a struct type IsExample struct { Message string TypeErrorMessage string } // Create a factory func NewIsExample() IsEven { return IsEven{ Message: \"This value does not equal \\\"example\\\".\", TypeErrorMessage: \"This value can not be processed.\", } } // Implement the validation func (c IsExample) Validate(data any) []Error { errors := []Error{} // Should not validate blank data if len(v.NewNotBlank().Validate(data)) != 0 { return []Error{} } t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: if data.(string) != \"example\" { errors = append(errors, Error(c.Message)) } default: errors = append(errors, Error(c.TypeErrorMessage)) } return errors } ","import#Import":" import ( \"gitnet.fr/deblan/go-form/validation\" ) "},"title":"Constraints"},"/go-form/docs/fields/":{"data":{"":"","#":"A field represents a field in a form.\nCheckbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbox(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=checkbox]\nChoice func NewFieldChoice(name string) *Field field := form.NewFieldChoice(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates inputs (checkbox or radio) or selects\nCsrf func NewFieldCsrf(name string) *Field field := form.NewFieldCsrf(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Date func NewFieldDate(name string) *Field field := form.NewFieldDate(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=date] with default transformers\nDatetime func NewFieldDatetime(name string) *Field field := form.NewFieldDatetime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime] with default transformers\nDatetimeLocal func NewFieldDatetimeLocal(name string) *Field field := form.NewFieldDatetimeLocal(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=datetime-local] with default transformers\nHidden func NewFieldHidden(name string) *Field field := form.NewFieldHidden(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=hidden]\nMail func NewFieldMail(name string) *Field field := form.NewFieldMail(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=email]\nNumber func NewFieldNumber(name string) *Field field := form.NewFieldNumber(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=number] with default transformers\nPassword func NewFieldPassword(name string) *Field field := form.NewFieldPassword(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=password]\nRange func NewFieldRange(name string) *Field field := form.NewFieldRange(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=range]\nSub Form func NewFieldSubForm(name string) *Field field := form.NewFieldSubForm(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Alias:\nfunc NewSubForm(name string) *Field Generates a sub form\nText func NewFieldText(name string) *Field field := form.NewFieldText(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=text]\nTextarea func NewFieldTextarea(name string) *Field field := form.NewFieldTextarea(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates a textarea\nTime func NewFieldTime(name string) *Field field := form.NewFieldTime(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=time] with default transformers\nSubmit func NewSubmit(name string) *Field field := form.NewSubmit(\"Foo\") Test me Run Try it yourself ↗ Share ↗ Generates an input[type=submit]","methods#Methods":"Add func (f *Field) Add(children ...*Field) *Field Appends children\nBind func (f *Field) Bind(data map[string]any, key *string) error Bind the data into the given map\nGetChild func (f *Field) GetChild(name string) *Field Returns a child using its name\nGetId func (f *Field) GetId() string Computes the id of the field\nGetName func (f *Field) GetName() string Computes the name of the field\nGetOption func (f *Field) GetOption(name string) *Option Returns an option using its name\nHasChild func (f *Field) HasChild(name string) bool Checks if the field contains a child using its name\nHasOption func (f *Field) HasOption(name string) bool Checks if the field contains an option using its name\nMount func (f *Field) Mount(data any) error Populates the field with data\nResetErrors func (f *Field) ResetErrors() *Field Resets the field errors\nWithBeforeBind func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field Sets a transformer applied to the data of a field before defining it in a structure\nWithBeforeMount func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field Sets a transformer applied to the structure data before displaying it in a field\nWithConstraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field Appends constraints\nWithData func (f *Field) WithData(data any) *Field Sets data the field\nWithFixedName func (f *Field) WithFixedName() *Field Sets that the name of the field is not computed\nWithOptions func (f *Field) WithOptions(options ...*Option) *Field Common options Name type description Info required bool Add required=\"true\" Does not apply a constraint attr form.Attrs List of extra attributes of the field row_attr form.Attrs List of extra attributes of the field’s top container label string The label of the field Usually show before the field label_attr form.Attrs List of extra attributes of the label help string Helper of the field help_attr form.Attrs List of extra attributes of the help Appends options to the field\nWithSlice func (f *Field) WithSlice() *Field Sets that the field represents a data slice"},"title":"Fields"},"/go-form/docs/form/":{"data":{"":"","example#Example":"Prerequisites import ( \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/validation\" ) type Person struct { Name string Age int } Creating a form myForm := form.NewForm( form.NewFieldText(\"Name\"). WithConstraints( validation.NewNotBlank(), ), form.NewFieldNumber(\"Age\"). WithConstraints( validation.NewNotBlank(), validation.NewRange().WithMin(18), ), ).End() Validating a struct data := Person{} myForm.Mount(data) myForm.IsValid() // false data = Person{ Name: \"Alice\", Age: 42, } myForm.Mount(data) myForm.IsValid() // true Validating a request import ( \"net/http\" ) myForm.WithMethod(http.MethodPost) // req *http.Request if req.Method == myForm.Method { myForm.HandleRequest(req) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } ","methods#Methods":"NewForm func NewForm(fields ...*Field) *Form Generates a new form with default properties\nAdd func (f *Form) Add(fields ...*Field) Appends children\nAddGlobalField func (f *Form) AddGlobalField(field *Field) Configures its children deeply\nBind func (f *Form) Bind(data any) error Copies datas from the form to a struct\nEnd func (f *Form) End() *Form Configures its children deeply This function must be called after adding all\nfields\nGetField func (f *Form) GetField(name string) *Field Returns a child using its name\nGetOption func (f *Form) GetOption(name string) *Option Returns an option using its name\nHandleRequest func (f *Form) HandleRequest(req *http.Request) Processes a request\nHasField func (f *Form) HasField(name string) bool Checks if the form contains a child using its name\nHasOption func (f *Form) HasOption(name string) bool Checks if the form contains an option using its name\nIsSubmitted func (f *Form) IsSubmitted() bool Checks if the form is submitted\nIsValid func (f *Form) IsValid() bool Checks the a form is valid\nMount func (f *Form) Mount(data any) error Copies datas from a struct to the form\nResetErrors func (f *Form) ResetErrors() *Form Resets the form errors\nWithAction func (f *Form) WithAction(v string) *Form Sets the action of the form (eg: “/”)\nWithMethod func (f *Form) WithMethod(v string) *Form Sets the method of the format (http.MethodPost, http.MethodGet, …)\nWithName func (f *Form) WithName(v string) *Form Sets the name of the form (used to compute name of fields)\nWithOptions func (f *Form) WithOptions(options ...*Option) *Form Appends options to the form\nOptions Name Type Description attr map[string]string List of extra attributes help string Helper ","struct#Struct":" type Form struct { Fields []*Field GlobalFields []*Field Errors []validation.Error Method string Action string Name string Options []*Option RequestData *url.Values } "},"title":"Form"},"/go-form/docs/installation/":{"data":{"":" go get gitnet.fr/deblan/go-form "},"title":"Installation"},"/go-form/docs/rendering/":{"data":{"":"go-form allows you to render a form using Go’s built-in template engine. Here is a simple example that displays a form:\nmyForm := form.NewForm(...) render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(` \u003chtml\u003e \u003chead\u003e \u003ctitle\u003eMy form\u003c/title\u003e \u003c/head\u003e \u003cbody\u003e {{ form .Form }} \u003c/body\u003e \u003c/html\u003e `) b := new(strings.Builder) tpl.Execute(w, map[string]any{ \"Form\": myForm, }) fmt.Println(b.String()) @import \"fmt\" @import \"html/template\" @import \"strings\" @import @import \"github.com/yosssi/gohtml\" @import \"gitnet.fr/deblan/go-form/example\" @import \"gitnet.fr/deblan/go-form/theme\" form := example.CreateDataForm() render := theme.NewRenderer(theme.Html5) // render := theme.NewRenderer(theme.Bootstrap5) tpl, _ := template.New(\"page\").Funcs(render.FuncMap()).Parse(`{{ form .Form }}`) buff := new(strings.Builder) tpl.Execute(buff, map[string]any{ \"Form\": form, }) fmt.Println(gohtml.Format(buff.String())) Test me Run Try it yourself ↗ Share ↗ Other helper functions are available to render specific parts of the form:\nform_errors: displays the form’s global errors form_row : renders the label, errors, and widget of a field form_label: renders only the label of a field form_widget: renders only the widget of a field form_widget_errors: renders only the errors of a specific field "},"title":"Rendering"},"/go-form/docs/rendering/theming/":{"data":{"":"go-form provides 2 themes:\ntheme.Html5: a basic view without classes theme.Bootstrap5: a theme for Bootstrap 5 You can add a custom theme. Learn by reading the Bootstrap5 theme."},"title":"Theming"},"/go-form/docs/workflow/":{"data":{"":"","#":" Import import ( \"html/template\" \"net/http\" \"gitnet.fr/deblan/go-form/form\" \"gitnet.fr/deblan/go-form/theme\" ) Create a form // Let's create a new form // You can pass *form.Field as arguments myForm := form.NewForm(field1, field2, ...) // Add somes fields myForm.Add(field3, field4, ...) // Set the method // \u003cform method=\"POST\" ...\u003e myForm.WithMethod(http.MethodPost) // Define the action // \u003cform action=\"/\" ...\u003e myForm.WithAction(\"/\") // Set a name myForm.WithName(\"myForm\") // Add options myForm.WithOptions(option1, option2, ...) // When all fields are added, call End() myForm.End() Attributes Some options are natively supported in go-form themes.\nmyForm.WithOptions( form.NewOption(\"help\", \"A help for the form\"), // \u003cform data-foo=\"bar\" data-bar=\"bar\" ... form.NewOption(\"attr\", form.Attrs{ \"data-foo\": \"foo\", \"data-bar\": \"bar\", }), ) Data mounting This step is not required when you does not want to pre-fill the form. Your struct can be complexe and the form only map existing properties.\ntype Person struct { Name string Age int } data := Person{ Name: \"Alice\", Age: 42, } // Assuming 2 fields named \"Name\" and \"Age\" exist myForm.Mount(data) Rendering http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // myForm := form.NewForm(...) 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\": myForm, }) } Data binding This is the final step. After the form handles the request, you can check if the form has been submitted, check the values are valid and finally populate your struct.\nhttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) { // data := Person{...} // myForm := form.NewForm(...) if r.Method == myForm.Method { myForm.HandleRequest(r) if myForm.IsSubmitted() \u0026\u0026 myForm.IsValid() { myForm.Bind(\u0026data) } } }) "},"title":"Workflow"}} \ No newline at end of file From fa5582ee4f83364b808aef80c921f7246a5b5db2 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Jul 2025 21:53:24 +0200 Subject: [PATCH 076/117] fix(theme): checkbox is check on nil value --- theme/html5.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/html5.go b/theme/html5.go index a5803a9..2a85aee 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -185,7 +185,7 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { ID(field.GetId()), Type(fieldType), Value(value), - If(fieldType == "checkbox" && field.Data != false, Checked()), + If(fieldType == "checkbox" && field.Data != nil && field.Data != false, Checked()), If(field.HasOption("required") && field.GetOption("required").AsBool(), Required()), parent["input_attributes"](parent, field), ) From 2296b945b221229e8b298929fd4135dafcf98842 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Jul 2025 21:53:43 +0200 Subject: [PATCH 077/117] feat(example): remove boostrap classes --- example/form.go | 53 +------------------------------------------------ 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/example/form.go b/example/form.go index d945a99..458f33b 100644 --- a/example/form.go +++ b/example/form.go @@ -64,9 +64,6 @@ func CreateDataForm() *form.Form { WithOptions( form.NewOption("label", "Bytes"), form.NewOption("required", true), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ). WithBeforeMount(func(data any) (any, error) { return cast.ToString(data), nil @@ -81,9 +78,6 @@ func CreateDataForm() *form.Form { WithOptions( form.NewOption("label", "Text"), form.NewOption("help", "Must contain 'deblan'"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ). WithConstraints( validation.NewRegex(`deblan`), @@ -91,9 +85,6 @@ func CreateDataForm() *form.Form { form.NewFieldCheckbox("Checkbox"). WithOptions( form.NewOption("label", "Checkbox"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ), form.NewSubForm("Inputs"). WithOptions( @@ -103,9 +94,6 @@ func CreateDataForm() *form.Form { form.NewFieldNumber("Number"). WithOptions( form.NewOption("label", "Number"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ). WithConstraints( validation.NewRange().WithRange(1, 20), @@ -114,16 +102,10 @@ func CreateDataForm() *form.Form { form.NewFieldRange("Range"). WithOptions( form.NewOption("label", "Range"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ), form.NewFieldMail("Mail"). WithOptions( form.NewOption("label", "Mail"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ). WithConstraints( validation.Mail{}, @@ -131,9 +113,6 @@ func CreateDataForm() *form.Form { form.NewFieldPassword("Password"). WithOptions( form.NewOption("label", "Password"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ). WithConstraints( validation.NewLength().WithMin(10), @@ -147,30 +126,18 @@ func CreateDataForm() *form.Form { form.NewFieldDate("Date"). WithOptions( form.NewOption("label", "Date"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ), form.NewFieldDatetime("DateTime"). WithOptions( form.NewOption("label", "Datetime"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ), form.NewFieldDatetimeLocal("DateTimeLocal"). WithOptions( form.NewOption("label", "DateTime local"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ), form.NewFieldTime("Time"). WithOptions( form.NewOption("label", "Time"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ), ), form.NewSubForm("Choices"). @@ -180,9 +147,6 @@ func CreateDataForm() *form.Form { WithOptions( form.NewOption("choices", itemsChoices), form.NewOption("label", "Select"), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ). WithConstraints( validation.NewNotBlank(), @@ -192,9 +156,6 @@ func CreateDataForm() *form.Form { form.NewOption("choices", itemsChoices), form.NewOption("label", "Select (expanded)"), form.NewOption("expanded", true), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ), form.NewFieldChoice("MultipleSelect"). WithSlice(). @@ -202,9 +163,6 @@ func CreateDataForm() *form.Form { form.NewOption("choices", itemsChoices), form.NewOption("label", "Multiple select"), form.NewOption("multiple", true), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ). WithConstraints( validation.NewNotBlank(), @@ -217,9 +175,6 @@ func CreateDataForm() *form.Form { form.NewOption("label", "Multiple select (expanded)"), form.NewOption("expanded", true), form.NewOption("multiple", true), - form.NewOption("row_attr", form.Attrs{ - "class": "col-12 mb-3", - }), ), ), form.NewFieldCsrf("_csrf_token").WithData("my-token"), @@ -232,13 +187,7 @@ func CreateDataForm() *form.Form { ). End(). WithOptions( - form.NewOption("help", "form help"), - form.NewOption("help_attr", form.Attrs{ - "class": "btn btn-primary", - }), - form.NewOption("attr", form.Attrs{ - "class": "row", - }), + form.NewOption("help", "Form help"), ). WithMethod(http.MethodPost). WithAction("/") From 6963fbad79f7060ce04ec4be89902451d1c8b01b Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Jul 2025 21:54:54 +0200 Subject: [PATCH 078/117] update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 195158a..d2c0db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## [Unreleased] +## v1.1.2 + +### Added + +- chore(example): remove boostrap classes + +### Fixed + +- fix(theme): checkbox is check on nil value + ## v1.1.1 ### Fixed From 290d8ad61394db4c3533c42cf3bd973b7f571b06 Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 28 Jul 2025 19:58:25 +0000 Subject: [PATCH 079/117] Build doc --- js/custom.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/custom.js b/js/custom.js index 3b7f770..1322d0d 100644 --- a/js/custom.js +++ b/js/custom.js @@ -102,12 +102,12 @@ function createFieldPlaygroundCode(code) { return `package main import ( - 'fmt' - 'html/template' - 'strings' + "fmt" + "html/template" + "strings" - 'gitnet.fr/deblan/go-form/form' - 'gitnet.fr/deblan/go-form/theme' + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/theme" ) func main() { From ec81cb23d3646471fd6a2cadcc447c28e48c968f Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 28 Jul 2025 19:59:47 +0000 Subject: [PATCH 080/117] Build doc --- js/custom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/custom.js b/js/custom.js index 1322d0d..e8d886e 100644 --- a/js/custom.js +++ b/js/custom.js @@ -118,7 +118,7 @@ ${code} func r(f *form.Form) { render := theme.NewRenderer(theme.Html5) - tpl, _ := template.New('example').Funcs(render.FuncMap()).Parse(\`{{ form_widget (.Form.GetField "Foo") }}\`) + tpl, _ := template.New("example").Funcs(render.FuncMap()).Parse(\`{{ form_widget (.Form.GetField "Foo") }}\`) b := new(strings.Builder) tpl.Execute(b, map[string]any{"Form": f}) fmt.Println(b.String()) From b4e27adf1421afa6e15a3b5ac2611c459b42847b Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 28 Jul 2025 20:03:11 +0000 Subject: [PATCH 081/117] Build doc --- docs/fields/index.html | 32 ++++++++++++++++---------------- docs/rendering/index.html | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/fields/index.html b/docs/fields/index.html index 2327052..96412df 100644 --- a/docs/fields/index.html +++ b/docs/fields/index.html @@ -481,7 +481,7 @@ Checkbox func NewFieldCheckbox(name string) *Field field := form.NewFieldCheckbo
        Test me - +
        -
        - Data -
        {{ .Dump }}
        -
        + + + + + +
        +
        {{ .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") } From 97f5cf321560b7abd32bd58a77d590aa54f94cf9 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 26 Sep 2025 17:49:39 +0200 Subject: [PATCH 110/117] [WIP] feat: handle collection in form rendering, mount and bind --- form/field.go | 41 ++++++++++++++++++++++++++++++---------- form/field_collection.go | 33 +++++++++++++++++++------------- form/form.go | 11 +++-------- main.go | 30 +++++++++++++++++++++++++++-- theme/html5.go | 5 +---- 5 files changed, 83 insertions(+), 37 deletions(-) diff --git a/form/field.go b/form/field.go index dee37bf..b4484df 100644 --- a/form/field.go +++ b/form/field.go @@ -21,20 +21,13 @@ import ( "slices" "strings" + "github.com/yassinebenaid/godump" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) // 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 @@ -47,6 +40,10 @@ func FieldValidation(f *Field) bool { } isValid = isValid && isChildValid + + for _, sc := range c.Children { + isValid = isValid && FieldValidation(sc) + } } return isValid @@ -115,6 +112,7 @@ func NewField(name, widget string) *Field { func (f *Field) Copy() *Field { return &Field{ Name: f.Name, + Form: f.Form, Widget: f.Widget, Options: f.Options, Constraints: f.Constraints, @@ -332,7 +330,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) error { +func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error { if len(f.Children) == 0 { v, err := f.BeforeBind(f.Data) @@ -352,7 +350,30 @@ func (f *Field) Bind(data map[string]any, key *string) error { data[f.Name] = make(map[string]any) for _, child := range f.Children { - child.Bind(data[f.Name].(map[string]any), key) + 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 { + godump.Dump([]string{valueKey, key}) + nextData = append(nextData, value) + } + } + } + + data[f.Name] = nextData } return nil diff --git a/form/field_collection.go b/form/field_collection.go index 436ea27..52b8e02 100644 --- a/form/field_collection.go +++ b/form/field_collection.go @@ -41,22 +41,29 @@ func NewFieldCollection(name string) *Field { slice := reflect.ValueOf(data) for i := 0; i < slice.Len(); i++ { - form := src.Copy() - form.Mount(slice.Index(i).Interface()) + name := fmt.Sprintf("%d", i) + value := 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") + if f.HasChild(name) { + f.GetChild(name).Mount(value) + } else { + form := src.Copy() + form.Mount(value) - for _, c := range field.Children { - c.NamePrefix = fmt.Sprintf("[%d]", i) + 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) } - - f.Add(field) } } } diff --git a/form/form.go b/form/form.go index b96df95..c17da05 100644 --- a/form/form.go +++ b/form/form.go @@ -25,7 +25,6 @@ import ( "strings" "github.com/mitchellh/mapstructure" - "github.com/yassinebenaid/godump" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) @@ -110,10 +109,8 @@ func (f *Form) End() *Form { func (f *Form) AddGlobalField(field *Field) { f.GlobalFields = append(f.GlobalFields, field) - if field.Widget != "collection" { - for _, c := range field.Children { - f.AddGlobalField(c) - } + for _, c := range field.Children { + f.AddGlobalField(c) } } @@ -221,11 +218,9 @@ func (f *Form) Bind(data any) error { toBind := make(map[string]any) for _, field := range f.Fields { - field.Bind(toBind, nil) + field.Bind(toBind, nil, false) } - godump.Dump(toBind) - return mapstructure.Decode(toBind, data) } diff --git a/main.go b/main.go index da58d53..7c4e97a 100644 --- a/main.go +++ b/main.go @@ -16,8 +16,6 @@ func main() { 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() @@ -113,6 +111,34 @@ func main() {
        {{ form .Form }} + + `) diff --git a/theme/html5.go b/theme/html5.go index a709e4e..a4728e0 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -375,10 +375,7 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { field.WithOptions(form.NewOption("prototype", prototype)) field.Widget = "collection_build" - return Div( - Attr("data-prototype", prototype), - parent["form_widget"](parent, field), - ) + return parent["form_widget"](parent, field) } theme["collection_build"] = func(parent map[string]RenderFunc, args ...any) Node { From b9c5f6a2fd77130d02b5cc0d2a908047564a1bca Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 16:30:27 +0200 Subject: [PATCH 111/117] fix(collection): always validate fields when another field is invalid" --- form/field.go | 10 ++++------ form/field_choice.go | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/form/field.go b/form/field.go index b4484df..f1558c2 100644 --- a/form/field.go +++ b/form/field.go @@ -21,13 +21,12 @@ import ( "slices" "strings" - "github.com/yassinebenaid/godump" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) // Generic function for field.Validation -func FieldValidation(f *Field) bool { +func DefaultFieldValidation(f *Field) bool { if len(f.Children) > 0 { isValid := true @@ -39,10 +38,10 @@ func FieldValidation(f *Field) bool { c.Errors = errs } - isValid = isValid && isChildValid + isValid = isChildValid && isValid for _, sc := range c.Children { - isValid = isValid && FieldValidation(sc) + isValid = DefaultFieldValidation(sc) && isValid } } @@ -104,7 +103,7 @@ func NewField(name, widget string) *Field { NewOption("help_attr", Attrs{}), ) - f.Validate = FieldValidation + f.Validate = DefaultFieldValidation return f } @@ -367,7 +366,6 @@ func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error for _, key := range keys { for valueKey, value := range values { if valueKey == key { - godump.Dump([]string{valueKey, key}) nextData = append(nextData, value) } } diff --git a/form/field_choice.go b/form/field_choice.go index 70d2545..16c06fc 100644 --- a/form/field_choice.go +++ b/form/field_choice.go @@ -145,7 +145,7 @@ func NewFieldChoice(name string) *Field { ) f.Validate = func(field *Field) bool { - isValid := FieldValidation(field) + isValid := field.Validate(field) if len(validation.NewNotBlank().Validate(field.Data)) == 0 { choices := field.GetOption("choices").Value.(*Choices) From 1d40aa6b09135368915c10216dcb11dce98bb05c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 18:13:43 +0200 Subject: [PATCH 112/117] refactor: refactor the example --- example.go | 76 +++++++++++++ example/form.go | 32 +++--- example/view/bootstrap.html | 113 +++++++++++++++++++ example/view/html5.html | 133 ++++++++++++++++++++++ main.go | 219 ------------------------------------ 5 files changed, 338 insertions(+), 235 deletions(-) create mode 100644 example.go create mode 100644 example/view/bootstrap.html create mode 100644 example/view/html5.html delete mode 100644 main.go diff --git a/example.go b/example.go new file mode 100644 index 0000000..2aa6727 --- /dev/null +++ b/example.go @@ -0,0 +1,76 @@ +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{} + form := example.CreateDataForm(action) + + form.Mount(entity) + + if r.Method == form.Method { + form.HandleRequest(r) + + if form.IsSubmitted() && form.IsValid() { + form.Bind(&entity) + } + } + + content, _ := templates.ReadFile(view) + + formAsJson, _ := json.MarshalIndent(form, " ", " ") + + 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": form.IsSubmitted(), + "isValid": form.IsValid(), + "form": form, + "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.Fatal(http.ListenAndServe(":1122", nil)) +} diff --git a/example/form.go b/example/form.go index 4fd035c..8c516ba 100644 --- a/example/form.go +++ b/example/form.go @@ -50,7 +50,7 @@ type CollectionItem struct { ValueB string } -func CreateDataForm() *form.Form { +func CreateDataForm(action string) *form.Form { items := []Item{ Item{Id: 1, Name: "Item 1"}, Item{Id: 2, Name: "Item 2"}, @@ -66,19 +66,6 @@ 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"), @@ -196,6 +183,19 @@ func CreateDataForm() *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( @@ -206,8 +206,8 @@ func CreateDataForm() *form.Form { ). End(). WithOptions( - form.NewOption("help", "Form help"), + form.NewOption("help", "Form global help"), ). WithMethod(http.MethodPost). - WithAction("/") + WithAction(action) } diff --git a/example/view/bootstrap.html b/example/view/bootstrap.html new file mode 100644 index 0000000..7b848f1 --- /dev/null +++ b/example/view/bootstrap.html @@ -0,0 +1,113 @@ + + + + + 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 new file mode 100644 index 0000000..90c33fb --- /dev/null +++ b/example/view/html5.html @@ -0,0 +1,133 @@ + + + + + Form HTML5 (with 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/main.go b/main.go deleted file mode 100644 index 7c4e97a..0000000 --- a/main.go +++ /dev/null @@ -1,219 +0,0 @@ -package main - -import ( - "encoding/json" - "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{} - data.Collection = []example.CollectionItem{ - {"Value a 1", "Value b 1"}, - } - - 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 }} -
        - - - - - -
        -
        {{ .Dump }}
        -
        -
        - JSON -
        {{ .Json }}
        -
        -
        -
        - - {{ form .Form }} - - - - - `) - - 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)), - }) - }) - - 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 }} -
        -
        - Data -
        {{ .Dump }}
        -
        -
        - - {{ 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)) -} From bec0acd2f2e333e7317eb36cfc1211f09c43fb5f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 18:14:08 +0200 Subject: [PATCH 113/117] feat(theme/html5): add classes for help and errors html nodes --- theme/html5.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/theme/html5.go b/theme/html5.go index a4728e0..2d7889e 100644 --- a/theme/html5.go +++ b/theme/html5.go @@ -59,6 +59,7 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { } return Ul( + Class("gf-errors"), Group(result), ) } @@ -94,6 +95,7 @@ var Html5 = CreateTheme(func() map[string]RenderFunc { } return Div( + Class("gf-help"), Text(help), extra, ) @@ -452,6 +454,7 @@ 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), From 7373b1921235164584cd9a9d292118a61012d3e6 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 18:15:23 +0200 Subject: [PATCH 114/117] doc: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f052f98..12a7ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Added + +- feat: add collection widget + ## v1.4.0 ### Added From f70f0a1f9d191307163268fe864aa5476eedc6ed Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 23:23:13 +0200 Subject: [PATCH 115/117] feat(example): add theme picker --- example.go | 25 ++++++++++++++---------- example/form.go | 38 +++++++++++++++++++++++++++++++++++++ example/view/bootstrap.html | 12 +++++++++--- example/view/html5.html | 9 +++++++-- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/example.go b/example.go index 2aa6727..91918d4 100644 --- a/example.go +++ b/example.go @@ -17,21 +17,24 @@ var templates embed.FS func handler(view, action string, formRenderer *theme.Renderer, w http.ResponseWriter, r *http.Request) { entity := example.ExampleData{} - form := example.CreateDataForm(action) + entityForm := example.CreateDataForm(action) + entityForm.Mount(entity) - form.Mount(entity) + style := example.NewTheme(action) + styleForm := example.CreateThemeSelectorForm() + styleForm.Mount(style) - if r.Method == form.Method { - form.HandleRequest(r) + if r.Method == entityForm.Method { + entityForm.HandleRequest(r) - if form.IsSubmitted() && form.IsValid() { - form.Bind(&entity) + if entityForm.IsSubmitted() && entityForm.IsValid() { + entityForm.Bind(&entity) } } content, _ := templates.ReadFile(view) - formAsJson, _ := json.MarshalIndent(form, " ", " ") + formAsJson, _ := json.MarshalIndent(entityForm, " ", " ") tpl, _ := template.New("page"). Funcs(formRenderer.FuncMap()). @@ -43,9 +46,10 @@ func handler(view, action string, formRenderer *theme.Renderer, w http.ResponseW dump.Theme = godump.Theme{} tpl.Execute(w, map[string]any{ - "isSubmitted": form.IsSubmitted(), - "isValid": form.IsValid(), - "form": form, + "isSubmitted": entityForm.IsSubmitted(), + "isValid": entityForm.IsValid(), + "form": entityForm, + "styleForm": styleForm, "json": string(formAsJson), "dump": template.HTML(dump.Sprint(entity)), }) @@ -72,5 +76,6 @@ func main() { ) }) + log.Println("Browse: http://localhost:1122") log.Fatal(http.ListenAndServe(":1122", nil)) } diff --git a/example/form.go b/example/form.go index 8c516ba..fc6e8bf 100644 --- a/example/form.go +++ b/example/form.go @@ -50,6 +50,10 @@ type CollectionItem struct { ValueB string } +type Theme struct { + Value string `field:"lowerCamel"` +} + func CreateDataForm(action string) *form.Form { items := []Item{ Item{Id: 1, Name: "Item 1"}, @@ -211,3 +215,37 @@ func CreateDataForm(action string) *form.Form { 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) +} diff --git a/example/view/bootstrap.html b/example/view/bootstrap.html index 7b848f1..23543fd 100644 --- a/example/view/bootstrap.html +++ b/example/view/bootstrap.html @@ -2,11 +2,12 @@ - Form with Bootstrap + Demo of go-form with Bootstrap -
        +
        + {{ form_widget (.styleForm.GetField "value") }} +
        + +

        Demo of go-form with Bootstrap

        +
        Debug view
        diff --git a/example/view/html5.html b/example/view/html5.html index 90c33fb..88db2d8 100644 --- a/example/view/html5.html +++ b/example/view/html5.html @@ -2,7 +2,7 @@ - Form HTML5 (with Pico) + Demo of go-form (pure HTML5 and Pico) @@ -41,7 +41,12 @@ } - + + {{ form_widget (.styleForm.GetField "value") }} +
        + +

        Demo of go-form (pure HTML5 and Pico)

        +
        Debug view From fe5d84d200a2e7dd53d2b46870fd7f31656d4813 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 23:24:42 +0200 Subject: [PATCH 116/117] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a7ade..ba56fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ ## [Unreleased] +## v1.5.0 + ### Added - feat: add collection widget +- feat: refactoring and improvement of example ## v1.4.0 From f451d69d702b350f7361de930153b88f4638015f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 1 Oct 2025 23:30:16 +0200 Subject: [PATCH 117/117] doc: add documentation of functions and licence --- form/field.go | 2 ++ form/field_collection.go | 10 +++++----- form/form.go | 1 + util/collection.go | 15 +++++++++++++++ util/transformer.go | 15 +++++++++++++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/form/field.go b/form/field.go index f1558c2..fe63326 100644 --- a/form/field.go +++ b/form/field.go @@ -158,6 +158,7 @@ func (f *Field) WithOptions(options ...*Option) *Field { return f } +// Remove an option if exists func (f *Field) RemoveOption(name string) *Field { var options []*Option @@ -377,6 +378,7 @@ func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error return nil } +// Generates a tree of errors func (f *Field) ErrorsTree(tree map[string]any, key *string) { var index string diff --git a/form/field_collection.go b/form/field_collection.go index 52b8e02..3f3ece1 100644 --- a/form/field_collection.go +++ b/form/field_collection.go @@ -1,10 +1,5 @@ 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 @@ -20,6 +15,11 @@ import ( // 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"). diff --git a/form/form.go b/form/form.go index c17da05..04016d3 100644 --- a/form/form.go +++ b/form/form.go @@ -293,6 +293,7 @@ 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) diff --git a/util/collection.go b/util/collection.go index 7ee4d66..f0aa649 100644 --- a/util/collection.go +++ b/util/collection.go @@ -1,5 +1,20 @@ 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" diff --git a/util/transformer.go b/util/transformer.go index 743b117..a942fa1 100644 --- a/util/transformer.go +++ b/util/transformer.go @@ -1,5 +1,20 @@ 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"