Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ The library ships structure, the project owns the look (CSS-first doctrine):
3. **`form.SetGlobalClass("my-app-form")`** — adds classes to every `<form>`
created afterwards, useful for scoping: `.my-app-form .tw-field { ... }`.

4. **`f.SetClass("my-form")`** — appends classes to a specific form instance
on top of any global classes. Chainable.

## Built-in Input Types

18 types in `input/` ([full reference](docs/STANDARD_TYPES.md)):
Expand All @@ -190,7 +193,8 @@ The library ships structure, the project owns the look (CSS-first doctrine):

Need one that isn't here? **Custom inputs** live in your own package: embed
`input.Base`, configure the `Permitted` rules, override `Validate` if needed.
Full pattern: [input/README.md](input/README.md).
If your input needs custom markup, implement the `form.Renderer` capability
interface — see [input/README.md](input/README.md).

## API Reference

Expand All @@ -206,8 +210,10 @@ where the name comes from the optional `Namer` interface
|--------|-------------|
| `String() string` | Generates form HTML |
| `Render() *dom.Element` | **WASM** — reactive DOM tree (`dom.ViewRenderer`) |
| `Submit() error` | Runs sync + validate + OnSubmit callback programmatically; returns first validation error |
| `SetSSR(bool) *Form` | SSR mode: adds `method`/`action` attributes |
| `OnSubmit(func(model.Fielder, func(error))) *Form` | WASM submit callback |
| `SetClass(...string) *Form` | Appends CSS classes to this form (on top of SetGlobalClass) |
| `Validate() error` | Validates all inputs, returns first error |
| `SyncValues(model.Fielder) error` | Copies input values back into the data struct |
| `ValidateData(byte, model.Fielder) error` | Server-side validation (crudp.DataValidator) |
Expand Down
27 changes: 27 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ For each field in `data.Schema()`:
4. `field.NotNull` → `SetRequired(true)` on the input.
5. Current value bound via `fmt.ReadValues()` + `SetValues()`.

## `(*Form).Submit()` — Submit Pipeline

Runs the full submission pipeline programmatically:
1. `SyncValues(data)` — syncs input values to the struct.
2. `Validate()` — performs final validation.
3. If valid and `OnSubmit` is set:
- Sets the `submitting` signal to `true`.
- Fires the `onSubmit(data, done)` callback.
- Resets the form on `done(nil)` unless `NoResetOnSuccess()` was called.

The DOM `submit` handler delegates to this method after `PreventDefault()`.
Returns the first validation error, or `nil` if submission was dispatched.

## `(*Form).Validate()` — Validation Detail

- Skips fields with `SkipValidation` set to true in the input.
Expand Down Expand Up @@ -56,6 +69,20 @@ Error messages from `Permitted.Validate(name, text)`:
- `"space not allowed"` — space when Spaces=false
- `"character {X} not allowed"` — disallowed character

## `form.Renderer` — Custom Input Markup

Optional capability interface for custom inputs that own their markup:

```go
type Renderer interface {
RenderInput(value *dom.SignalString, onInput func(string)) *dom.Element
}
```

- **Contract**: The form still owns the field wrapper (`div.tw-field`), the error span, and validation.
- **Wiring**: The widget must call `onInput` with the new value whenever the user interacts with the control; the form then updates the value signal and runs live validation.
- **Location**: Defined in package `form` (not `input`) because it references `*dom.Element`.

## Namer Interface

Fielder types can optionally implement `Namer` to provide a custom form name:
Expand Down
38 changes: 38 additions & 0 deletions form.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,44 @@ func (f *Form) SetOptions(fieldName string, opts ...fmt.KeyValue) *Form {
// Reset clears all input values and error messages in the DOM and internal state.
func (f *Form) Reset() { f.reset() }

// Submit runs the full submit pipeline programmatically: syncs input values
// into the bound struct, validates, and (if valid) fires the OnSubmit
// callback. Returns the first validation error, or nil if the submission
// was dispatched. The async result of the submission itself is delivered
// through the OnSubmit callback's done function.
func (f *Form) Submit() error {
// Sync all values from signals to struct
f.SyncValues(f.data)

// Validate all (final check)
if err := f.Validate(); err != nil {
return err
}

if f.onSubmit != nil {
f.submitting.Set(true)
f.onSubmit(f.data, func(err error) {
f.submitting.Set(false)
if err == nil && !f.noResetOnSuccess {
f.reset()
}
})
}
return nil
}

// SetClass appends CSS classes to this form (on top of any global classes
// set via SetGlobalClass). Chainable.
func (f *Form) SetClass(classes ...string) *Form {
for _, c := range classes {
if f.class != "" {
f.class += " "
}
f.class += c
}
return f
}

func (f *Form) resolveSubmitLabel() string {
label := f.submitLabel
if label == "" {
Expand Down
23 changes: 23 additions & 0 deletions input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,29 @@ The custom input renders with the generic markup for its `htmlName`
`checkbox` → bool, else text); override `Storage()` on your struct if your
kind needs a different mapping.

### Custom markup (Renderer)

If your input needs a special UI (e.g. a color picker, a file uploader, or a
composite widget), implement the `form.Renderer` interface in your struct.
The `form` package will call this instead of its default switch:

```go
import (
"github.com/tinywasm/dom"
"github.com/tinywasm/form"
)

func (m *myInput) RenderInput(value *dom.SignalString, onInput func(string)) *dom.Element {
el := dom.NewElement("div").Class("my-custom-widget")
// ... build your UI, bind 'value' signal for updates
// ... call 'onInput(newValue)' when the user interacts
return el
}
```

The `form.Renderer` interface lives in the root `form` package because it
references `dom.Element`; the `input` package remains dom-free by contract.

### Base Available Methods

| Method | Purpose |
Expand Down
19 changes: 1 addition & 18 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,7 @@ func (f *Form) Render() *dom.Element {
// Bind submit event
el.On("submit", func(e dom.Event) {
e.PreventDefault()

// Sync all values from signals to struct
f.SyncValues(f.data)

// Validate all (final check)
if err := f.Validate(); err != nil {
return
}

if f.onSubmit != nil {
f.submitting.Set(true)
f.onSubmit(f.data, func(err error) {
f.submitting.Set(false)
if err == nil && !f.noResetOnSuccess {
f.reset()
}
})
}
f.Submit()
})

return el
Expand Down
36 changes: 26 additions & 10 deletions render_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ type fieldComponent struct {
err *dom.SignalString
}

// Renderer is an optional capability for custom inputs that own their markup.
// The form still owns the field wrapper (div.tw-field), the error span, and
// validation: the widget must call onInput with the new value on user input —
// the form updates the value signal and runs live validation. The value
// signal carries the initial value and programmatic updates (SetValues).
type Renderer interface {
RenderInput(value *dom.SignalString, onInput func(string)) *dom.Element
}

func (fc *fieldComponent) String() string {
return fc.Render().String()
}
Expand Down Expand Up @@ -39,16 +48,23 @@ func (fc *fieldComponent) validate(val string) {
func (fc *fieldComponent) Render() *dom.Element {
container := dom.NewElement("div").Class("tw-field")

htmlName := fc.Input.HTMLName()
switch htmlName {
case "radio":
fc.renderRadio(container)
case "select":
fc.renderSelect(container)
case "datalist":
fc.renderDatalist(container)
default:
fc.renderInput(container)
if r, ok := fc.Input.(Renderer); ok {
container.Child(r.RenderInput(fc.value, func(v string) {
fc.value.Set(v)
fc.validate(v)
}))
} else {
htmlName := fc.Input.HTMLName()
switch htmlName {
case "radio":
fc.renderRadio(container)
case "select":
fc.renderSelect(container)
case "datalist":
fc.renderDatalist(container)
default:
fc.renderInput(container)
}
}

errSpan := dom.NewElement("span").
Expand Down
86 changes: 0 additions & 86 deletions submit.shared_test.go

This file was deleted.

Loading