refactor: refactor the example

This commit is contained in:
Simon Vieille 2025-10-01 18:13:43 +02:00
commit 1d40aa6b09
Signed by: deblan
GPG key ID: 579388D585F70417
5 changed files with 338 additions and 235 deletions

76
example.go Normal file
View file

@ -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))
}

View file

@ -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)
}

113
example/view/bootstrap.html Normal file
View file

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form with Bootstrap</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
<style>
fieldset {
border: 1px solid #999;
padding: 10px;
}
form > div:not(:last-child), fieldset > div:not(:last-child) {
padding-bottom: 20px;
}
.gf-help {
font-style: italic;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<details>
<summary>Debug view</summary>
<div class="py-2">
<strong>Submitted:</strong>
{{ .isSubmitted }}
</div>
<div class="py-2">
<strong>Valid:</strong>
{{ .isValid }}
</div>
<details class="py-2">
<summary><strong>Dump of data</strong></summary>
<pre class="p-2">{{ .dump }}</pre>
</details>
<details class="py-2">
<summary><strong>Form as JSON</strong></summary>
<pre class="p-2">{{ .json }}</pre>
</details>
</details>
{{if .isValid}}
<div class="alert alert-success">The form is valid!</div>
{{else}}
<div class="alert alert-warning">The form is invalid!</div>
{{end}}
{{ form .form }}
</div>
<script>
const collections = document.querySelectorAll('*[data-prototype]')
const collectionItemAddToolBar = (item) => {
const toolbar = document.createElement('div')
const createBtn = () => {
const btn = document.createElement('button')
btn.textContent = '-'
btn.type = 'button'
btn.className = "btn btn-primary"
btn.addEventListener('click', () => {
item.remove()
})
return btn
}
toolbar.appendChild(createBtn())
item.querySelector('fieldset').appendChild(toolbar)
}
const collectionAddToolbar = (collection) => {
const container = collection.parentNode
const toolbar = document.createElement('div')
const createBtn = () => {
const btn = document.createElement('button')
btn.textContent = '+'
btn.type = 'button'
btn.className = "btn btn-primary"
btn.addEventListener('click', () => {
const form = collection.getAttribute('data-prototype')
.replace(/__name__/g, collection.children.length)
collection.insertAdjacentHTML("beforeend", form)
collectionItemAddToolBar(collection.lastChild)
})
return btn
}
toolbar.appendChild(createBtn())
container.appendChild(toolbar)
}
collections.forEach((collection) => {
collectionAddToolbar(collection)
for (let item of collection.children) {
collectionItemAddToolBar(item)
}
})
</script>
</body>
</html>

133
example/view/html5.html Normal file
View file

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form HTML5 (with Pico)</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<style>
* {
font-size: 15px;
}
.p10 {
padding-top: 10px;
padding-bottom: 10px;
}
pre {
padding: 10px;
}
fieldset {
border: 1px solid var(--pico-form-element-border-color);
padding: 10px;
}
form > div:not(:last-child), fieldset > div:not(:last-child) {
padding-bottom: 20px;
}
.gf-errors {
color: var(--pico-form-element-invalid-border-color);
}
.gf-help {
font-style: italic;
color: var(--pico-form-element-placeholder-color);
}
</style>
</head>
<body class="container">
<details class="p10">
<summary>Debug view</summary>
<div class="p10">
<strong>Submitted:</strong>
{{ .isSubmitted }}
</div>
<div class="p10">
<strong>Valid:</strong>
{{ .isValid }}
</div>
<details class="p10">
<summary><strong>Dump of data</strong></summary>
<pre>{{ .dump }}</pre>
</details>
<details class="p10">
<summary><strong>Form as JSON</strong></summary>
<pre>{{ .json }}</pre>
</details>
</div>
</details>
{{if .isValid}}
<p class="pico-color-green-500">The form is valid!</p>
{{else}}
<p class="pico-color-red-500">The form is invalid!</p>
{{end}}
{{ form .form }}
<script>
const collections = document.querySelectorAll('*[data-prototype]')
const collectionItemAddToolBar = (item) => {
const toolbar = document.createElement('div')
const createBtn = () => {
const btn = document.createElement('button')
btn.textContent = '-'
btn.type = 'button'
btn.addEventListener('click', () => {
item.remove()
})
return btn
}
toolbar.appendChild(createBtn())
item.querySelector('fieldset').appendChild(toolbar)
}
const collectionAddToolbar = (collection) => {
const container = collection.parentNode
const toolbar = document.createElement('div')
const createBtn = () => {
const btn = document.createElement('button')
btn.textContent = '+'
btn.type = 'button'
btn.addEventListener('click', () => {
const form = collection.getAttribute('data-prototype')
.replace(/__name__/g, collection.children.length)
collection.insertAdjacentHTML("beforeend", form)
collectionItemAddToolBar(collection.lastChild)
})
return btn
}
toolbar.appendChild(createBtn())
container.appendChild(toolbar)
}
collections.forEach((collection) => {
collectionAddToolbar(collection)
for (let item of collection.children) {
collectionItemAddToolBar(item)
}
})
</script>
</body>
</html>

219
main.go
View file

@ -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(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form</title>
<style>
input[type="text"],
input[type="date"],
input[type="datetime"],
input[type="time"],
input[type="range"],
input[type="email"],
input[type="number"],
input[type="password"],
select,
input[type="datetime-local"],
textarea {
box-sizing: border-box;
padding: 9px;
margin: 10px 0;
display: block;
width: 100%;
border: 1px solid black;
}
.form-errors {
margin: 0;
padding: 5px 0 0 0;
color: red;
list-style: none;
}
.form-errors li {
padding: 0;
margin: 0;
}
.form-help {
color: blue;
font-size: 9px;
}
.debug {
padding: 10px;
}
.debug .debug-value {
color: #555;
padding: 10px 0 0 10px;
}
</style>
</head>
<body>
<div class="debug">
<div>
<strong>Submitted</strong>
<span class="debug-value">{{ .Form.IsSubmitted }}</span>
</div>
<div>
<strong>Valid</strong>
<span class="debug-value">{{ .Form.IsValid }}</span>
</div>
<table width="100%">
<tr>
<td width="50%" valign="top">
<pre class="debug-valid">{{ .Dump }}</pre>
</td>
<td valign="top">
<details>
<summary>JSON</summary>
<pre class="debug-valid">{{ .Json }}</pre>
</details>
</td>
</tr>
</table>
</div>
{{ form .Form }}
<script>
const collections = document.querySelectorAll('*[data-prototype]')
collections.forEach((collection) => {
const container = collection.parentNode
const toolbar = document.createElement('div')
const createAdd = () => {
const btn = document.createElement('button')
btn.textContent = '+'
btn.type = 'button'
const form = collection.getAttribute('data-prototype')
.replace(/__name__/g, collection.children.length)
btn.addEventListener('click', () => {
collection.insertAdjacentHTML("beforeend", form)
})
return btn
}
toolbar.appendChild(createAdd())
container.appendChild(toolbar)
})
</script>
</body>
</html>
`)
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(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
</head>
<body>
<div class="container">
<div class="list-group">
<div class="list-group-item">
<strong>Submitted</strong>
<span class="debug-value">{{ .Form.IsSubmitted }}</span>
</div>
<div class="list-group-item">
<strong>Valid</strong>
<span class="debug-value">{{ .Form.IsValid }}</span>
</div>
<div class="list-group-item">
<strong>Data</strong>
<pre class="debug-valid">{{ .Dump }}</pre>
</div>
</div>
{{ form .Form }}
</div>
</body>
</html>
`)
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))
}