mirror of
https://codeberg.org/readeck/readeck.git
synced 2026-06-18 11:04:36 +00:00
241 lines
5.3 KiB
Go
241 lines
5.3 KiB
Go
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
// Package forms provide form related components.
|
|
package forms
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/a-h/templ"
|
|
|
|
"codeberg.org/readeck/readeck/pkg/forms"
|
|
)
|
|
|
|
// Binder is a [forms.Binder] with a Name method.
|
|
type Binder interface {
|
|
forms.Binder
|
|
Name() string
|
|
}
|
|
|
|
// TypedBinder is a typed [Binder].
|
|
type TypedBinder[T any] interface {
|
|
forms.TypedBinder[T]
|
|
Binder
|
|
}
|
|
|
|
type baseWidget struct {
|
|
name string
|
|
value any
|
|
label string
|
|
help string
|
|
required bool
|
|
classes templ.CSSClasses
|
|
controllAttrs templ.Attributes
|
|
inputType string
|
|
inputClasses templ.CSSClasses
|
|
inputAttrs templ.Attributes
|
|
}
|
|
|
|
// FieldOption is a function to set [baseWidget] properties.
|
|
type FieldOption func(f *baseWidget)
|
|
|
|
type fieldWidget struct {
|
|
Binder
|
|
baseWidget
|
|
}
|
|
|
|
func widget(field Binder, options ...FieldOption) fieldWidget {
|
|
f := fieldWidget{
|
|
Binder: field,
|
|
baseWidget: baseWidget{
|
|
name: field.Name(),
|
|
value: field.V(),
|
|
classes: templ.CSSClasses{},
|
|
controllAttrs: templ.Attributes{},
|
|
inputClasses: templ.CSSClasses{},
|
|
inputAttrs: templ.Attributes{},
|
|
},
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(&f.baseWidget)
|
|
}
|
|
|
|
return f
|
|
}
|
|
|
|
func (f *fieldWidget) ariaAttrs() (attrs templ.Attributes) {
|
|
attrs = templ.Attributes{}
|
|
|
|
if f.help != "" {
|
|
attrs["aria-describedby"] = "description-" + f.name
|
|
}
|
|
if len(f.Errors()) > 0 {
|
|
attrs["aria-errormessage"] = "errors-" + f.name
|
|
attrs["aria-invalid"] = "true"
|
|
}
|
|
return attrs
|
|
}
|
|
|
|
// Value set the field's value.
|
|
func Value(v any) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.value = v
|
|
}
|
|
}
|
|
|
|
// Name sets the field's name.
|
|
func Name(v string) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.name = v
|
|
}
|
|
}
|
|
|
|
// Label sets the field's label.
|
|
func Label(v string) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.label = v
|
|
}
|
|
}
|
|
|
|
// Help sets the field's help.
|
|
func Help(v string) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.help = v
|
|
}
|
|
}
|
|
|
|
// Required sets the field's required flag.
|
|
func Required(v bool) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.required = v
|
|
}
|
|
}
|
|
|
|
// Classes sets the field's classes.
|
|
func Classes(args ...any) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.classes = templ.Classes(args...)
|
|
}
|
|
}
|
|
|
|
// ControlAttrs sets the field's control attributes.
|
|
func ControlAttrs(attrs templ.Attributes) FieldOption {
|
|
return func(f *baseWidget) {
|
|
maps.Copy(f.controllAttrs, attrs)
|
|
}
|
|
}
|
|
|
|
// InputType sets the field's input type.
|
|
// Works only for <input> elements.
|
|
func InputType(v string) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.inputType = v
|
|
}
|
|
}
|
|
|
|
// InputClasses sets the field's input classes.
|
|
func InputClasses(args ...any) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.inputClasses = templ.Classes(args...)
|
|
}
|
|
}
|
|
|
|
// InputAttr adds a attribute to the field's input.
|
|
func InputAttr(name string, value any) FieldOption {
|
|
return func(f *baseWidget) {
|
|
f.inputAttrs[name] = value
|
|
}
|
|
}
|
|
|
|
// InputAttrs sets the field's input attributes.
|
|
func InputAttrs(attrs templ.Attributes) FieldOption {
|
|
return func(f *baseWidget) {
|
|
maps.Copy(f.inputAttrs, attrs)
|
|
}
|
|
}
|
|
|
|
type textField struct {
|
|
fieldWidget
|
|
}
|
|
|
|
// TextField renders a text field (text, email, etc.)
|
|
func TextField(field Binder, options ...FieldOption) templ.Component {
|
|
return (&textField{widget(
|
|
field,
|
|
append([]FieldOption{InputType("text")}, options...)...,
|
|
)}).component()
|
|
}
|
|
|
|
// DateField renders a [TextField] with a date type.
|
|
func DateField(field Binder, options ...FieldOption) templ.Component {
|
|
if v, ok := field.V().(time.Time); ok && !v.IsZero() {
|
|
options = slices.Insert(options, 0, Value(v.Format(time.DateOnly)))
|
|
}
|
|
return TextField(
|
|
field,
|
|
append([]FieldOption{InputType("date")}, options...)...,
|
|
)
|
|
}
|
|
|
|
type textAreaField struct {
|
|
fieldWidget
|
|
}
|
|
|
|
// TextAreaField renders a textarea field.
|
|
func TextAreaField(field Binder, options ...FieldOption) templ.Component {
|
|
return (&textAreaField{widget(field, options...)}).component()
|
|
}
|
|
|
|
type checkboxField struct {
|
|
fieldWidget
|
|
}
|
|
|
|
// CheckboxField renders a checkbox field.
|
|
func CheckboxField(field TypedBinder[bool], options ...FieldOption) templ.Component {
|
|
return (&checkboxField{widget(field, options...)}).component()
|
|
}
|
|
|
|
type selectField[T comparable] struct {
|
|
fieldWidget
|
|
}
|
|
|
|
// SelectField renders a select field with options.
|
|
func SelectField[T comparable](field Binder, options ...FieldOption) templ.Component {
|
|
return (&selectField[T]{widget(field, options...)}).component()
|
|
}
|
|
|
|
type multiSelectField[T comparable] struct {
|
|
fieldWidget
|
|
}
|
|
|
|
// MultiSelectField renders a list of checkboxes.
|
|
func MultiSelectField[T comparable](field Binder, options ...FieldOption) templ.Component {
|
|
return (&multiSelectField[T]{widget(field, options...)}).component()
|
|
}
|
|
|
|
type passwordField struct {
|
|
fieldWidget
|
|
}
|
|
|
|
// PasswordField renders a password field with a reveal controller.
|
|
func PasswordField(field Binder, options ...FieldOption) templ.Component {
|
|
return (&passwordField{widget(
|
|
field,
|
|
append([]FieldOption{InputType("text")}, options...)...,
|
|
)}).component()
|
|
}
|
|
|
|
type timeTokenField struct {
|
|
fieldWidget
|
|
}
|
|
|
|
// TimeTokenField renders a field with a helper to select time tokens.
|
|
func TimeTokenField(field Binder, options ...FieldOption) templ.Component {
|
|
return (&timeTokenField{widget(field, options...)}).component()
|
|
}
|