init
This commit is contained in:
commit
d735f6e472
14 changed files with 608 additions and 0 deletions
172
form/field.go
Normal file
172
form/field.go
Normal file
|
|
@ -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
|
||||
}
|
||||
8
form/field_input.go
Normal file
8
form/field_input.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package form
|
||||
|
||||
func NewFieldText(name string) *Field {
|
||||
f := NewField(name, "input").
|
||||
WithOptions(Option{Name: "type", Value: "text"})
|
||||
|
||||
return f
|
||||
}
|
||||
11
form/field_subform.go
Normal file
11
form/field_subform.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
117
form/form.go
Normal file
117
form/form.go
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
6
form/option.go
Normal file
6
form/option.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package form
|
||||
|
||||
type Option struct {
|
||||
Name string
|
||||
Value any
|
||||
}
|
||||
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module gitnet.fr/deblan/go-form
|
||||
|
||||
go 1.23.0
|
||||
77
main.go
Normal file
77
main.go
Normal file
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
27
theme/html5.go
Normal file
27
theme/html5.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package theme
|
||||
|
||||
var Html5 = map[string]string{
|
||||
"form": `<form action="" method="">
|
||||
{{ form_error .Form nil }}
|
||||
{{ .Content }}
|
||||
</form>`,
|
||||
"label": `<label for="">Label</label>`,
|
||||
"input": `<input name="{{ .Field.Name }}" value="{{ .Field.Data }}" type="text">`,
|
||||
"sub_form": `
|
||||
{{ form_label .Field }}
|
||||
|
||||
{{ range $field := .Field.Children }}
|
||||
{{ form_row $field }}
|
||||
{{ end }}
|
||||
`,
|
||||
"error": `<div class="error">
|
||||
{{ range $error := .Errors }}
|
||||
{{ $error }}<br>
|
||||
{{ end }}
|
||||
</div>`,
|
||||
"row": `<div class="row">
|
||||
{{ form_label .Field }}
|
||||
{{ form_error nil .Field }}
|
||||
{{ form_widget .Field }}
|
||||
</div>`,
|
||||
}
|
||||
90
theme/renderer.go
Normal file
90
theme/renderer.go
Normal file
|
|
@ -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())
|
||||
}
|
||||
30
util/inspect.go
Normal file
30
util/inspect.go
Normal file
|
|
@ -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
|
||||
}
|
||||
5
validation/constraint.go
Normal file
5
validation/constraint.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package validation
|
||||
|
||||
type Constraint interface {
|
||||
Validate(data any) []Error
|
||||
}
|
||||
3
validation/error.go
Normal file
3
validation/error.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package validation
|
||||
|
||||
type Error string
|
||||
46
validation/notblank.go
Normal file
46
validation/notblank.go
Normal file
|
|
@ -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
|
||||
}
|
||||
13
validation/validation.go
Normal file
13
validation/validation.go
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue