Skip to content

feat: Forms & Validation framework #79

@MCGPPeters

Description

@MCGPPeters

Problem Statement

Abies currently has no built-in forms or validation support. Developers must manually wire up every form input with oninput → Message → Update → model field, and implement all validation logic from scratch. This is the most verbose gap compared to Blazor, which provides EditForm, DataAnnotationsValidator, ValidationSummary, InputText, InputNumber, and other typed input components with automatic validation.

Current State (Conduit example)

In Abies.Conduit, every form requires extensive boilerplate:

// Login form - every field needs manual wiring
input([
    class_("form-control form-control-lg"),
    type("email"),
    placeholder("Email"),
    value(model.Email),
    oninput(v => new Message.EmailChanged(v ?? ""))
], [])

// Validation is entirely manual
if (model.Errors is not null)
{
    foreach (var error in model.Errors)
    {
        li([], [text(error)])
    }
}

This pattern is repeated across Login, Register, Settings, and Editor pages — each with its own model fields, change messages, and error display logic.

Proposed Solution

Add a forms module to Abies that stays true to the MVU architecture while reducing boilerplate. The key insight is that forms are a pattern, not a component — we can provide helper functions that generate the right virtual DOM nodes and messages.

Three layers:

  1. Abies.Forms module — Higher-level input helpers that bundle value + oninput + validation display
  2. Abies.Validation module — Pure validation functions that work with any model
  3. Integration with the existing MVU loop — No magic, no hidden state, forms are still just Node trees and messages

API Design

Layer 1: Form Helpers

using static Abies.Forms;

// Instead of manually wiring value + oninput + validation:
InputText(
    model.Email,
    v => new EmailChanged(v),
    validation: Validate.Required("Email is required")
                       .And(Validate.Email("Must be a valid email")),
    placeholder: "Email",
    class_: "form-control form-control-lg"
)

// Typed inputs
InputNumber(model.Age, v => new AgeChanged(v), min: 0, max: 150)
InputPassword(model.Password, v => new PasswordChanged(v))
TextArea(model.Bio, v => new BioChanged(v), rows: 8)

// Select dropdown
Select(model.SelectedTag, v => new TagSelected(v), options: tags)

Layer 2: Validation

using static Abies.Validation;

// Pure validation functions — return ValidationResult
public static ValidationResult ValidateLogin(LoginModel model) =>
    Validate.All(
        Validate.Required(model.Email, "Email is required"),
        Validate.Email(model.Email, "Must be a valid email"),
        Validate.MinLength(model.Password, 8, "Password must be at least 8 characters")
    );

// ValidationResult is a simple sum type
public interface ValidationResult
{
    record Valid : ValidationResult;
    record Invalid(IReadOnlyList<string> Errors) : ValidationResult;
}

Layer 3: Form wrapper

// Form helper that prevents submit when invalid
Form(
    onValidSubmit: new SubmitLogin(),
    validation: ValidateLogin(model),
    children: [
        InputText(model.Email, v => new EmailChanged(v), ...),
        InputPassword(model.Password, v => new PasswordChanged(v), ...),
        SubmitButton("Sign in", model.IsSubmitting)
    ]
)

Alternatives Considered

  1. Blazor-style EditContext with DataAnnotations — Rejected because it relies on mutable state and reflection, which conflicts with the MVU pattern
  2. Two-way binding macro — Rejected because Abies is explicitly one-directional (message-based)
  3. Do nothing, keep manual wiring — Viable but leads to significant boilerplate in real apps (Conduit has ~200 lines of manual form wiring)

Acceptance Criteria

  • Abies.Forms module with InputText, InputPassword, InputNumber, TextArea, Select helpers
  • Abies.Validation module with Required, MinLength, MaxLength, Email, Pattern, Range, Custom validators
  • ValidationResult sum type (Valid / Invalid)
  • Validate.All() combinator for composing validators
  • Form() helper with onValidSubmit that checks validation before dispatching
  • ValidationSummary() and ValidationMessage() display helpers
  • All helpers return standard Node types — no hidden state
  • Unit tests for all validators
  • Update Conduit app to use the new forms module (Login, Register, Settings, Editor)
  • Documentation with examples

Priority

🔴 High — Core functionality gap

Additional Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions