// SPDX-FileCopyrightText: © 2026 Olivier Meunier // // SPDX-License-Identifier: AGPL-3.0-only package forms import ( "maps" "slices" "codeberg.org/readeck/readeck/pkg/forms" . "codeberg.org/readeck/readeck/components" ) templ Errors(form forms.FormBinder) { if !form.IsValid() { } } templ (f *fieldWidget) component() { {{ f.classes = slices.Insert(f.classes, 0, "field") if len(f.Errors()) > 0 { f.classes = append(f.classes, "field--err with-errors") } }}
if f.label != "" { }
{ children... } if errors := f.Errors(); len(errors) > 0 {
    for _, e := range errors {
  • { e.Error() }
  • }
} if f.help != "" { { f.help } }
} templ (f *textField) component() { {{ f.classes = slices.Insert(f.classes, 0, "field--text") if len(f.inputClasses) == 0 { f.inputClasses = templ.Classes("form-input w-full") } if f.required { f.inputAttrs["required"] = true } if f.inputType != "password" { f.inputAttrs["value"] = f.value } else { f.inputAttrs["value"] = "" } parent := f.fieldWidget.component }} @parent() { { children... } } } templ (f *textAreaField) component() { {{ f.classes = slices.Insert(f.classes, 0, "field--textarea") if len(f.inputClasses) == 0 { f.inputClasses = templ.Classes("form-textarea w-full") } if _, ok := f.inputAttrs["rows"]; !ok { f.inputAttrs["rows"] = 3 } if f.value == nil { f.value = f.String() } if f.required { f.inputAttrs["required"] = true } parent := f.fieldWidget.component }} @parent() { } } templ (f *checkboxField) component() { {{ f.classes = slices.Insert(f.classes, 0, "field", "field--checkbox") if len(f.Errors()) > 0 { f.classes = append(f.classes, "field--err with-errors") } if v, ok := f.value.(bool); ok && v { f.inputAttrs["checked"] = "checked" } }}
// end with false so the field is bound if errors := f.Errors(); len(errors) > 0 {
    for _, e := range errors {
  • { e.Error() }
  • }
} if f.help != "" { { f.help } }
} templ (f *selectField[T]) component() { {{ choices := forms.ValueChoices[T]{} if fc, ok := f.Binder.(forms.ChoicesProvider[T]); ok { choices = fc.Choices() } if fc, ok := f.inputAttrs["choices"]; ok { delete(f.inputAttrs, "choices") if fc, ok := fc.(forms.ChoicesProvider[T]); ok { choices = fc.Choices() } } f.classes = slices.Insert(f.classes, 0, "field--select") if len(f.inputClasses) == 0 { f.inputClasses = templ.Classes("form-select w-full") } if f.required { f.inputAttrs["required"] = true } parent := f.fieldWidget.component }} @parent() { } } templ (f *multiSelectField[T]) component() { {{ f.classes = slices.Insert(f.classes, 0, "field", "field--multiselect") if len(f.Errors()) > 0 { f.classes = append(f.classes, "field--err with-errors") } choices := forms.ValueChoices[T]{} if fc, ok := f.Binder.(forms.ChoicesProvider[T]); ok { choices = fc.Choices() } if fc, ok := f.inputAttrs["choices"]; ok { delete(f.inputAttrs, "choices") if fc, ok := fc.(forms.ChoicesProvider[T]); ok { choices = fc.Choices() } } }}
// null value to bind the field
    {{ inputType := "checkbox" inputClasses := append(f.inputClasses, "form-checkbox") if _, ok := f.Binder.V().(T); ok { inputType = "radio" inputClasses = append(f.inputClasses, "form-radio") } }} for _, c := range choices { {{ checked := false if v, ok := f.Binder.V().(T); ok { checked = v == c.Value } else if v, ok := f.Binder.V().([]T); ok { checked = slices.Contains(v, c.Value) } }}
  • }
if errors := f.Errors(); len(errors) > 0 {
    for _, e := range errors {
  • { e.Error() }
  • }
} if f.help != "" { { f.help } }
} templ (f *passwordField) component() { {{ f.classes = slices.Insert(f.classes, 0, "field--text field--composed field--password") if len(f.inputClasses) == 0 { f.inputClasses = templ.Classes("form-input w-full") } f.controllAttrs["data-controller"] = "pass-reveal" if f.required { f.inputAttrs["required"] = true } f.inputAttrs["data-pass-reveal-target"] = "field" parent := f.fieldWidget.component }} @parent() {
} } templ (f *timeTokenField) component() { {{ f.classes = slices.Insert(f.classes, 0, "field--text field--composed field--timetoken") f.inputClasses = slices.Insert(f.inputClasses, 0, "form-input w-full") f.inputAttrs["data-timetoken-target"] = "field" if f.required { f.inputAttrs["required"] = true } parent := f.fieldWidget.component }} @parent() {
} } templ FiledDropField(field TypedBinder[forms.File], options ...FieldOption) { {{ f := textField{widget(field, options...)} f.inputType = "file" f.classes = slices.Insert(f.classes, 0, "field--filedrop") f.inputClasses = slices.Insert(f.inputClasses, 0, "js:hidden") maps.Copy(f.controllAttrs, templ.Attributes{ "data-controller": "dropzone", "data-dropzone-hidden-class": "hidden", "data-dropzone-focus-class": "bg-primary-100", }) parent := f.component }} @parent() {

} }