feat: replace templates with components

This commit is contained in:
Simon Vieille 2025-07-26 12:48:04 +02:00
commit 960d9175b1
Signed by: deblan
GPG key ID: 579388D585F70417
8 changed files with 572 additions and 400 deletions

View file

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

View file

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

6
go.mod
View file

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

6
go.sum
View file

@ -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=

View file

@ -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 <http://www.gnu.org/licenses/>.
var Bootstrap5 = map[string]string{
"form": `<form action="{{ .Form.Action }}" method="{{ .Form.Method }}" {{ form_attr .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 -}}
</form>`,
"attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`,
"help": `
{{- if gt (len .Help) 0 -}}
<div class="form-help">{{ .Help }}</div>
{{- end -}}
`,
"label": `
{{ if .Field.HasOption "label" }}
{{ $label := (.Field.GetOption "label").Value }}
{{- if ne $label "" -}}
<label for="{{ .Field.GetId }}" {{ form_label_attr .Field }} class="form-label">{{ $label }}</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 -}}
<input id="{{ .Field.GetId }}" {{ if $checked }}checked{{ end }} {{ if $required }}required="required"{{ end }} name="{{ .Field.GetName }}" value="{{ $value }}" type="{{ $type.Value }}" {{ form_widget_attr .Field }} class="{{ $class }}">
`,
"textarea": `
<textarea id="{{ .Field.GetId }}" {{ if .Field.HasOption "required" }}{{ if (.Field.GetOption "required").Value }}required="required"{{ end }}{{ end }} name="{{ .Field.GetName }}" {{ form_widget_attr .Field }} class="form-control">{{ .Field.Data }}</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) -}}
<div class="form-check">
<input value="" {{ if not $field.Data }}checked{{ end }} name="{{ $field.GetName }}" type="radio" id="{{ $field.GetId }}-0" class="form-check-input">
<label for="{{ $field.GetId }}-0" class="form-check-label">{{ ($field.GetOption "empty_choice_label").Value }}</label>
</div>
{{- end -}}
{{- range $key, $choice := $choices.GetChoices -}}
<div class="form-check">
<input name="{{ $field.GetName }}" type="{{ if $isMultiple }}checkbox{{ else }}radio{{ end }}" value="{{ $choice.Value }}" {{ if $choices.Match $field $choice.Value }}checked{{ end }} id="{{ $field.GetId }}-{{ sum $key $keyAdd }}" class="form-check-input">
<label for="{{ $field.GetId }}-{{ sum $key $keyAdd }}" class="form-check-label">{{- $choice.Label -}}</label>
</div>
{{- end -}}
{{- else -}}
<select id="{{ .Field.GetId }}" {{ if $required }}required="required"{{ end }} {{ if $isMultiple }}multiple{{ end }} name="{{ .Field.GetName }}" {{ form_widget_attr .Field }} class="form-select">
{{- if and (not $required) (not $isMultiple) -}}
<option value="">{{ $emptyChoiceLabel }}</option>
{{- end -}}
{{- range $choice := $choices.GetChoices -}}
<option value="{{ $choice.Value }}" {{ if $choices.Match $field $choice.Value }}selected{{ end }}>{{ $choice.Label }}</option>
{{- end -}}
</select>
{{- end -}}
`,
"sub_form": `
<fieldset id="{{ .Field.GetId }}">
{{ if .Field.HasOption "label" }}
{{ $label := (.Field.GetOption "label").Value }}
{{- if ne $label "" -}}
<legend {{ form_label_attr .Field }}>{{ $label }}</legend>
{{- end -}}
{{- end -}}
{{ form_widget_help .Field }}
{{- range $field := .Field.Children -}}
{{- form_row $field -}}
{{- end -}}
</fieldset>
`,
"error": `
{{- if gt (len .Errors) 0 -}}
<div class="invalid-feedback d-block">
{{- range $error := .Errors -}}
<div>{{- $error -}}</div>
{{- end -}}
</div>
{{- end -}}
`,
"row": `<div {{ form_row_attr .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_widget .Field -}}
{{- form_error nil .Field -}}
{{ if and (eq (len .Field.Children) 0) ($labelAfterWidget) }}
{{- form_label .Field -}}
{{ end }}
{{- form_widget_help .Field -}}
</div>`,
}
// var Bootstrap5 = map[string]string{
// "form": `<form action="{{ .Form.Action }}" method="{{ .Form.Method }}" {{ form_attr .Form }}>
// {{- form_error .Form nil -}}
//
// {{- form_help .Form -}}
//
// {{- range $field := .Form.Fields -}}
// {{- form_row $field -}}
// {{- end -}}
// </form>`,
// "attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`,
// "help": `
// {{- if gt (len .Help) 0 -}}
// <div class="form-help">{{ .Help }}</div>
// {{- end -}}
// `,
// "label": `
// {{ if .Field.HasOption "label" }}
// {{ $label := (.Field.GetOption "label").Value }}
//
// {{- if ne $label "" -}}
// <label for="{{ .Field.GetId }}" {{ form_label_attr .Field }} class="form-label">{{ $label }}</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 -}}
//
// <input id="{{ .Field.GetId }}" {{ if $checked }}checked{{ end }} {{ if $required }}required="required"{{ end }} name="{{ .Field.GetName }}" value="{{ $value }}" type="{{ $type.Value }}" {{ form_widget_attr .Field }} class="{{ $class }}">
// `,
// "textarea": `
// <textarea id="{{ .Field.GetId }}" {{ if .Field.HasOption "required" }}{{ if (.Field.GetOption "required").Value }}required="required"{{ end }}{{ end }} name="{{ .Field.GetName }}" {{ form_widget_attr .Field }} class="form-control">{{ .Field.Data }}</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) -}}
// <div class="form-check">
// <input value="" {{ if not $field.Data }}checked{{ end }} name="{{ $field.GetName }}" type="radio" id="{{ $field.GetId }}-0" class="form-check-input">
// <label for="{{ $field.GetId }}-0" class="form-check-label">{{ ($field.GetOption "empty_choice_label").Value }}</label>
// </div>
// {{- end -}}
//
// {{- range $key, $choice := $choices.GetChoices -}}
// <div class="form-check">
// <input name="{{ $field.GetName }}" type="{{ if $isMultiple }}checkbox{{ else }}radio{{ end }}" value="{{ $choice.Value }}" {{ if $choices.Match $field $choice.Value }}checked{{ end }} id="{{ $field.GetId }}-{{ sum $key $keyAdd }}" class="form-check-input">
// <label for="{{ $field.GetId }}-{{ sum $key $keyAdd }}" class="form-check-label">{{- $choice.Label -}}</label>
// </div>
// {{- end -}}
// {{- else -}}
// <select id="{{ .Field.GetId }}" {{ if $required }}required="required"{{ end }} {{ if $isMultiple }}multiple{{ end }} name="{{ .Field.GetName }}" {{ form_widget_attr .Field }} class="form-select">
// {{- if and (not $required) (not $isMultiple) -}}
// <option value="">{{ $emptyChoiceLabel }}</option>
// {{- end -}}
// {{- range $choice := $choices.GetChoices -}}
// <option value="{{ $choice.Value }}" {{ if $choices.Match $field $choice.Value }}selected{{ end }}>{{ $choice.Label }}</option>
// {{- end -}}
// </select>
// {{- end -}}
// `,
// "sub_form": `
// <fieldset id="{{ .Field.GetId }}">
// {{ if .Field.HasOption "label" }}
// {{ $label := (.Field.GetOption "label").Value }}
//
// {{- if ne $label "" -}}
// <legend {{ form_label_attr .Field }}>{{ $label }}</legend>
// {{- end -}}
// {{- end -}}
//
// {{ form_widget_help .Field }}
//
// {{- range $field := .Field.Children -}}
// {{- form_row $field -}}
// {{- end -}}
// </fieldset>
// `,
// "error": `
// {{- if gt (len .Errors) 0 -}}
// <div class="invalid-feedback d-block">
// {{- range $error := .Errors -}}
// <div>{{- $error -}}</div>
// {{- end -}}
// </div>
// {{- end -}}
// `,
// "row": `<div {{ form_row_attr .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_widget .Field -}}
// {{- form_error nil .Field -}}
//
// {{ if and (eq (len .Field.Children) 0) ($labelAfterWidget) }}
// {{- form_label .Field -}}
// {{ end }}
//
// {{- form_widget_help .Field -}}
// </div>`,
// }

View file

@ -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 <http://www.gnu.org/licenses/>.
var Html5 = map[string]string{
"form": `<form action="{{ .Form.Action }}" method="{{ .Form.Method }}" {{ form_attr .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 -}}
</form>`,
"attributes": `{{ range $key, $value := .Attributes }}{{ $key }}="{{ $value }}"{{ end }}`,
"help": `
{{- if gt (len .Help) 0 -}}
<div class="form-help">{{ .Help }}</div>
{{- 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 "" -}}
<label for="{{ .Field.GetId }}" {{ form_label_attr .Field }}>{{ $label }}</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)
<input id="{{ .Field.GetId }}" {{ if $checked }}checked{{ end }} {{ if $required }}required="required"{{ end }} name="{{ .Field.GetName }}" value="{{ $value }}" type="{{ $type.Value }}" {{ form_widget_attr .Field }}>
`,
"textarea": `
<textarea id="{{ .Field.GetId }}" {{ if .Field.HasOption "required" }}{{ if (.Field.GetOption "required").Value }}required="required"{{ end }}{{ end }} name="{{ .Field.GetName }}" {{ form_widget_attr .Field }}>{{ .Field.Data }}</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) -}}
<input value="" {{ if not $field.Data }}checked{{ end }} name="{{ $field.GetName }}" type="radio" id="{{ $field.GetId }}-0">
<label for="{{ $field.GetId }}-0">{{ ($field.GetOption "empty_choice_label").Value }}</label>
{{- end -}}
theme["errors"] = func(args ...any) Node {
errors := args[0].([]validation.Error)
{{- range $key, $choice := $choices.GetChoices -}}
<input name="{{ $field.GetName }}" type="{{ if $isMultiple }}checkbox{{ else }}radio{{ end }}" value="{{ $choice.Value }}" {{ if $choices.Match $field $choice.Value }}checked{{ end }} id="{{ $field.GetId }}-{{ sum $key $keyAdd }}">
<label for="{{ $field.GetId }}-{{ sum $key $keyAdd }}">{{- $choice.Label -}}</label>
{{- end -}}
{{- else -}}
<select id="{{ .Field.GetId }}" {{ if $required }}required="required"{{ end }} {{ if $isMultiple }}multiple{{ end }} name="{{ .Field.GetName }}" {{ form_widget_attr .Field }}>
{{- if and (not $required) (not $isMultiple) -}}
<option value="">{{ $emptyChoiceLabel }}</option>
{{- end -}}
{{- range $choice := $choices.GetChoices -}}
<option value="{{ $choice.Value }}" {{ if $choices.Match $field $choice.Value }}selected{{ end }}>{{ $choice.Label }}</option>
{{- end -}}
</select>
{{- end -}}
`,
"sub_form": `
<fieldset id="{{ .Field.GetId }}">
{{ if .Field.HasOption "label" }}
{{ $label := (.Field.GetOption "label").Value }}
var result []Node
{{- if ne $label "" -}}
<legend {{ form_label_attr .Field }}>{{ $label }}</legend>
{{- 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 -}}
</fieldset>
`,
"error": `
{{- if gt (len .Errors) 0 -}}
<ul class="form-errors">
{{- range $error := .Errors -}}
<li class="form-error">{{- $error -}}</li>
{{- end -}}
</ul>
{{- end -}}
`,
"row": `<div {{ form_row_attr .Field }}>
{{ $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 -}}
</div>`,
}
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
})

View file

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

16
theme/theme.go Normal file
View file

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