Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
donseba committed Feb 28, 2023
1 parent 77ca3ba commit 62afdd2
Show file tree
Hide file tree
Showing 9 changed files with 615 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@

# Dependency directories (remove the comment below to include it)
# vendor/
.idea/
110 changes: 109 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,110 @@
# go-form
Render forms in go based on struct layout

Render forms in go based on struct layout, and tags.

Please note that this package is a pre-alfa release and mainly only a visualization of my initial thought.
I am also trying to keep the footprint as low as possible without using third party packages.

it can convert to following data:
```go
data := struct {
Form ExampleForm
Errors []error
}{
Form: ExampleForm{
Name: "John Wick",
Email: "john.wick@gmail.com",
Address: &AddressBlock{
Street1: "121 Mill Neck",
City: "Long Island",
State: "NY",
Zip: "11765",
},
CheckBox: true,
CheckBox2: false,
},
Errors: []error{
fieldError{
Field: "Email",
Issue: "is already taken",
},
fieldError{
Field: "Address.Street1",
Issue: "is required",
},
},
}
```

into :
![](example/example-go-form.png)

Call `form_render` inside the template and pass it the `form struct` and the `errors` :
```html
<form class="space-y-6" action="#" method="POST">
{{ form_render .Form .Errors }}
<div class="flex items-center justify-between">
<button type="submit" class="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Signup</button>
</div>
</form>
```

There is currently only one template file for all the currently supported templates. I'm thinking of making a mini template for each type.
```html
<div>
<label {{with .Id}}for="{{.}}"{{end}} class="block text-sm font-medium text-gray-700">{{.Label}}{{ if eq .Required true }}*{{end}}</label>
<div class="mt-1">
{{ if eq .Type "dropdown" }}
<select {{with .Id}}id="{{.}}"{{end}} name="{{.Name}}" class="bg-white block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
{{ range $k, $option := .Values }}
<option value="{{$option.Id}}">{{$option.Name}}</option>
{{ end }}
</select>
{{ else if eq .Type "checkbox" }}
<input {{with .Id}}id="{{.}}"{{end}} name="{{.Name}}" type="checkbox" {{ if eq .Required true }}required{{end}} {{ if eq .Value true }}checked{{end}} class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
{{ else }}
<input {{with .Id}}id="{{.}}"{{end}} name="{{.Name}}" placeholder="{{.Placeholder}}" {{with .Value}}value="{{.}}"{{end}} {{ if eq .Required true }}required{{end}} class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
{{ end }}

{{range errors}}
<span class="text-sm text-red-600">{{.}}</span>
{{end}}
</div>
</div>
```

Groups (nested structs) have their own template
```html
<div class="mb-4 bg-gray-50 p-2 rounded-md">
<label class="block text-grey-darker text-sm font-bold mb-2">{{.Name }}</label>
{{ fields }}
</div>
```

please note that the errors need to implement with the `FieldError` interface. otherwise they will be silently skipped. You can achieve that doing so :

```go
type fieldError struct {
Field string
Issue string
}

func (fe fieldError) Error() string {
return fmt.Sprintf("%s:%s", fe.Field, fe.Issue)
}

func (fe fieldError) FieldError() (field, err string) {
return fe.Field, fe.Issue
}
```

supported tags
- label
- placeholder
- name
- required


## TODO
- better tag handling. maybe group all possible options into one tag or keep it as is.
- add validation to process the form once posted.
Binary file added example/example-go-form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package main

import (
"fmt"
"github.com/donseba/go-form"
"html/template"
"net/http"
)

var inputTpl = `<div>
<label {{with .Id}}for="{{.}}"{{end}} class="block text-sm font-medium text-gray-700">{{.Label}}{{ if eq .Required true }}*{{end}}</label>
<div class="mt-1">
{{ if eq .Type "dropdown" }}
<select {{with .Id}}id="{{.}}"{{end}} name="{{.Name}}" class="bg-white block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
{{ range $k, $option := .Values }}
<option value="{{$option.Id}}">{{$option.Name}}</option>
{{ end }}
</select>
{{ else if eq .Type "checkbox" }}
<input {{with .Id}}id="{{.}}"{{end}} name="{{.Name}}" type="checkbox" {{ if eq .Required true }}required{{end}} {{ if eq .Value true }}checked{{end}} class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
{{ else }}
<input {{with .Id}}id="{{.}}"{{end}} name="{{.Name}}" placeholder="{{.Placeholder}}" {{with .Value}}value="{{.}}"{{end}} {{ if eq .Required true }}required{{end}} class="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
{{ end }}
{{range errors}}
<span class="text-sm text-red-600">{{.}}</span>
{{end}}
</div>
</div>`

var groupTpl = `<div class="mb-4 bg-gray-50 p-2 rounded-md">
<label class="block text-grey-darker text-sm font-bold mb-2">
{{.Name }}
</label>
{{ fields }}
</div>`

func main() {
tpl := template.Must(template.New("").Funcs(template.FuncMap{
"errors": func() []form.FieldError { return nil },
}).Parse(inputTpl))
gtpl := template.Must(template.New("").Funcs(template.FuncMap{
"fields": func() template.HTML { return "" },
}).Parse(groupTpl))
fb := form.NewForm(*tpl, *gtpl)

pageTpl := template.Must(template.New("").Funcs(fb.FuncMap()).Parse(`
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-grey-lighter">
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form class="space-y-6" action="#" method="POST">
{{ form_render .Form .Errors }}
<div class="flex items-center justify-between">
<button type="submit" class="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Signup</button>
</div>
</form>
</div>
</div>
</body>
</html>
`))

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")

data := struct {
Form ExampleForm
Errors []form.FieldError
}{
Form: ExampleForm{
Name: "John Wick",
Email: "john.wick@gmail.com",
Address: &AddressBlock{
Street1: "121 Mill Neck",
City: "Long Island",
State: "NY",
Zip: "11765",
},
CheckBox: true,
CheckBox2: false,
},
Errors: []form.FieldError{
fieldError{
Field: "Email",
Issue: "is already taken",
},
fieldError{
Field: "Address.Street1",
Issue: "is required",
},
},
}

err := pageTpl.Execute(w, data)
if err != nil {
_, _ = fmt.Fprint(w, err)
return
}

})

err := http.ListenAndServe(":3000", nil)
if err != nil {
panic(err)
}
}

type ExampleForm struct {
Name string
Email string `required:"true"`
Address *AddressBlock
InputTypes form.InputFieldType `label:"Enum Example"`
CheckBox bool
CheckBox2 bool
}

type AddressBlock struct {
Street1 string
City string
State string
Zip string `label:"Postal Code"`
}

type fieldError struct {
Field string
Issue string
}

func (fe fieldError) Error() string {
return fmt.Sprintf("%s:%s", fe.Field, fe.Issue)
}

func (fe fieldError) FieldError() (field, err string) {
return fe.Field, fe.Issue
}
63 changes: 63 additions & 0 deletions fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package form

type FieldType string

const (
FieldTypeGroup FieldType = "group"
FieldTypeCheckbox FieldType = "checkbox"
FieldTypeChecklist FieldType = "checklist"
FieldTypeInput FieldType = "input"
FieldTypeLabel FieldType = "label"
FieldTypeRadios FieldType = "radios"
FieldTypeDropdown FieldType = "dropdown"
FieldTypeSubmit FieldType = "submit"
FieldTypeTextArea FieldType = "textArea"
)

type InputFieldType string

const (
InputFieldTypeText InputFieldType = "text"
InputFieldTypePassword InputFieldType = "password"
InputFieldTypeEmail InputFieldType = "email"
InputFieldTypeTel InputFieldType = "tel"
InputFieldTypeNumber InputFieldType = "number"
InputFieldTypeNone InputFieldType = ""
)

func (i InputFieldType) String() string {
return string(i)
}

func (i InputFieldType) Enum() []any {
return []interface{}{
InputFieldTypeText,
InputFieldTypePassword,
InputFieldTypeEmail,
InputFieldTypeTel,
InputFieldTypeNumber,
}
}

type FieldValue struct {
Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Group string `json:"group,omitempty"`
}

type FormField struct {
Id string `json:"id,omitempty"`
Placeholder string `json:"placeholder,omitempty"`
Name string `json:"name,omitempty"`
Value interface{} `json:"value,omitempty"`
Type FieldType `json:"type,omitempty"`
InputType InputFieldType `json:"inputType,omitempty"`
Label string `json:"label,omitempty"`
Step string `json:"step,omitempty"`
Values []FieldValue `json:"values,omitempty"`
Required bool `json:"required"`
Fields []FormField `json:"fields,omitempty"`
Legend string `json:"legend,omitempty"`
}
Loading

0 comments on commit 62afdd2

Please sign in to comment.