-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
615 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,4 @@ | |
|
||
# Dependency directories (remove the comment below to include it) | ||
# vendor/ | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
Oops, something went wrong.