Skip to content

Add support for submit/action buttons #17

Description

@Krever

The description below is AI-generated, so take it with a grian of salt, but it points in the right direction.

Summary

Add support for action/submit buttons as first-class form elements with callbacks, while maintaining effect-agnostic design.

Motivation

Currently, submit buttons must be handled outside the form system. Users need to:

  1. Manually render buttons in the app layer
  2. Wire up SubmitAttempted messages
  3. Handle form disabling during async operations manually

Proposed Design

Option A: Message-based (Elm-style) - Recommended

Keep buttons pure, emit typed messages that the app layer handles:

case class ActionButton(
  id: String,
  label: String,
  style: ButtonStyle = ButtonStyle.Primary  // Primary, Secondary, Danger, etc.
) extends FormElement

// In FormElementUpdate
case class ActionTriggered(buttonId: String) extends FormElementUpdate

App handles the effect:

case Msg.FormUpdated(ActionTriggered("submit")) =>
  (model.copy(formState = formState.setSubmitting), submitToServer())

Pros: Simple, fully effect-agnostic, consistent with current architecture
Cons: Callback logic lives in app, not with button definition

Option B: Callback with type parameter

Button carries a callback returning the app's message type:

case class ActionButton[Msg](
  id: String,
  label: String,
  onClick: () => Msg,
  disableFormWhileProcessing: Boolean = true
) extends FormElement

Pros: Callback co-located with button definition
Cons: Introduces type parameter, complicates derivation and state management

Option C: Effect-parameterized (tagless-style)

case class ActionButton[F[_]](
  id: String,
  label: String,
  onClick: F[Unit]
)

Pros: Explicit effect abstraction
Cons: Over-engineered for forms, complicates the API significantly

Form Disabling During Submission

Reuse LoadingState pattern from DataTable:

// New FormState wrapper or extension to root FormElementState
case class FormState(
  root: FormElementState,
  submissionState: SubmissionState = SubmissionState.Idle
)

enum SubmissionState {
  case Idle
  case Submitting(actionId: String)
  case Failed(error: String)
}

Renderers check submissionState to disable inputs and show loading indicators.

Derivation

Buttons should not participate in automatic form derivation from case classes. They must be added manually after derivation due to:

  • Custom callback logic per button
  • Multiple buttons with different behaviors (submit, save draft, cancel)
  • Button placement flexibility in the form layout

Example usage:

case class User(name: String, email: String) derives ToFormElem

val form = summon[ToFormElem[User]].get
val formWithButtons = form.withActions(
  ActionButton("submit", "Submit"),
  ActionButton("draft", "Save Draft", style = ButtonStyle.Secondary)
)

Open Questions

  1. Should multiple action buttons be supported in a single form?
  2. How to handle button-specific validation (e.g., "Save Draft" skips required field validation)?
  3. Should buttons have a disabled predicate based on form state (e.g., disable submit if form invalid)?

References

  • Elm's message-based approach: buttons emit messages, update function handles effects
  • Current DataTable server mode pattern: setLoading -> fetch -> setServerData

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions