Skip to content

Form Errors

Faststrap provides utilities for mapping backend validation errors to FormGroup components. These helpers make it easy to display server-side validation errors in a Bootstrap-compatible format.

Components and utilities in this module: - FormErrorSummary — renders a compact alert summary of all validation errors - FormGroupFromErrors — wraps a FormGroup and auto-applies error state from a backend error mapping - ValidationMessage — renders a single validation feedback message for one field - LiveValidationField — wraps an input with HTMX live validation attributes - extract_field_error — helper to extract a displayable error message for a specific field - map_formgroup_validation — returns FormGroup-ready validation flags for a field


Quick Start

from faststrap import FormErrorSummary, FormGroupFromErrors, map_formgroup_validation

# Server-side validation
@app.post("/login")
def login(email: str, password: str):
    errors = validate_input(email, password)
    if errors:
        return (
            FormErrorSummary(errors),
            FormGroupFromErrors(Input(name="email"), "email", errors=errors),
            FormGroupFromErrors(Input(name="password"), "password", errors=errors),
        )
    return RedirectResponse("/dashboard")

Visual Examples & Use Cases

1. Form Error Summary

Live Preview
FormErrorSummary({
    "email": "Invalid email address",
    "password": "Password must be at least 8 characters",
})

2. FormGroup with Error State

Live Preview
Invalid email address
FormGroup(
    Input(name="email", type="email", value="invalid"),
    error="Invalid email address",
    is_invalid=True,
)

3. Live Validation Field

Live Preview
from faststrap import LiveValidationField

LiveValidationField(
    Input(name="username"),
    validate_url="/validate/username",
    label="Username",
)

Practical Functionality

1. Complete Validation Flow

from faststrap import Form, FormGroup, FormErrorSummary, FormGroupFromErrors, Button

@app.post("/register")
def register(username: str, email: str, password: str):
    errors = validate_registration(username, email, password)
    if errors:
        return (
            FormErrorSummary(errors),
            FormGroupFromErrors(Input(name="username"), "username", errors=errors),
            FormGroupFromErrors(Input(name="email"), "email", errors=errors),
            FormGroupFromErrors(Input(name="password"), "password", errors=errors),
        )
    return RedirectResponse("/welcome")

2. Using extract_field_error

from faststrap import extract_field_error

# Extract single error for a field
error = extract_field_error(errors, "email")
# Returns "Invalid email address" or None

# Handles various error formats:
errors = {
    "email": "Invalid email address",
    "age": ["Must be 18+", "Must be under 100"],
    "name": {"msg": "Required"},
}

3. Live Validation Endpoint

@app.post("/validate/username")
def validate_username(username: str):
    if not is_available(username):
        return FormGroup(
            Input(name="username", value=username),
            error="Username is taken",
            is_invalid=True,
        )
    return FormGroup(
        Input(name="username", value=username),
        success="Username is available",
        is_valid=True,
    )

Parameter Reference

FormErrorSummary

Parameter Type Default Description
errors Mapping[str, Any] \| Iterable[str] \| str \| None required Validation errors to display.
title str "Please fix the following" Alert heading text.
variant str \| None None Bootstrap alert variant (defaults to "danger").
heading_cls str \| None None Extra classes for the heading element.
list_cls str \| None None Extra classes for the error list.
show_field_names bool True Prefix errors with field names.
dismissible bool \| None None Show dismiss button.
**kwargs Any {} Additional HTML attributes.

FormGroupFromErrors

Parameter Type Default Description
input_element Any required Input element to wrap.
field str required Field name to look up in errors.
errors Mapping[str, Any] \| None None Error mapping from the server.
**kwargs Any {} Additional FormGroup attributes.

LiveValidationField

Parameter Type Default Description
input_element Any required Input element to enhance.
validate_url str required HTMX validation endpoint.
label str \| None None Field label.
help_text str \| None None Help text below the field.
error str \| None None Initial error message.
success str \| None None Initial success message.
is_invalid bool False Show invalid state.
is_valid bool False Show valid state.
required bool False Show required indicator.
method "get" \| "post" "post" HTMX request method.
trigger str "blur changed delay:300ms" HTMX trigger string.
target str "closest .mb-3" HTMX target selector.
swap str "outerHTML" HTMX swap strategy.
indicator str \| None None HTMX loading indicator selector.
**kwargs Any {} Additional attributes.

API Reference

faststrap.components.forms.errors.FormErrorSummary(errors, *, title='Please fix the following', variant=UNSET, heading_cls=UNSET, list_cls=None, show_field_names=True, dismissible=UNSET, **kwargs)

Render a compact alert summary for validation errors.

Source code in src/faststrap/components/forms/errors.py
@register(category="forms")
def FormErrorSummary(
    errors: Mapping[str, Any] | Iterable[str] | str | None,
    *,
    title: str = "Please fix the following",
    variant: str | None = UNSET,
    heading_cls: str | None = UNSET,
    list_cls: str | None = None,
    show_field_names: bool = True,
    dismissible: bool | None = UNSET,
    **kwargs: Any,
) -> Any | None:
    """Render a compact alert summary for validation errors."""
    if not errors:
        return None

    cfg = resolve_defaults(
        "Alert",
        variant=variant,
        dismissible=dismissible,
        heading_cls=heading_cls,
    )
    c_variant = cfg.get("variant", variant)
    c_dismissible = cfg.get("dismissible", dismissible)
    c_heading_cls = cfg.get("heading_cls", heading_cls) or "alert-heading h6 mb-2"

    items: list[str] = []

    if isinstance(errors, Mapping):
        for field, _value in errors.items():
            message = extract_field_error(errors, field)
            if message is None:
                continue
            if show_field_names:
                items.append(f"{field}: {message}")
            else:
                items.append(message)
    elif isinstance(errors, str):
        items.append(errors)
    elif isinstance(errors, Iterable):
        for value in errors:
            if value is None:
                continue
            items.append(str(value))

    if not items:
        return None

    list_class = list_cls or "mb-0"
    list_el = Ul(*(Li(item) for item in items), cls=list_class)
    content = Div(
        H6(title, cls=c_heading_cls),
        list_el,
    )
    return Alert(
        content,
        variant=c_variant,
        dismissible=c_dismissible,
        **kwargs,
    )

faststrap.components.forms.errors.FormGroupFromErrors(input_element, field, errors=None, **kwargs)

Build FormGroup and auto-apply backend error state for one field.

Source code in src/faststrap/components/forms/errors.py
@register(category="forms")
def FormGroupFromErrors(
    input_element: Any,
    field: str,
    errors: Mapping[str, Any] | None = None,
    **kwargs: Any,
) -> Any:
    """Build FormGroup and auto-apply backend error state for one field."""
    mapping = map_formgroup_validation(errors, field)
    return FormGroup(input_element, **mapping, **kwargs)

faststrap.components.forms.errors.ValidationMessage(message, *, state='invalid', **kwargs)

Render a Bootstrap-compatible validation feedback message.

This is useful for HTMX live validation endpoints that return only the small feedback fragment for one field.

Source code in src/faststrap/components/forms/errors.py
@register(category="forms")
def ValidationMessage(
    message: str | None,
    *,
    state: Literal["invalid", "valid", "neutral"] = "invalid",
    **kwargs: Any,
) -> Div | None:
    """Render a Bootstrap-compatible validation feedback message.

    This is useful for HTMX live validation endpoints that return only the
    small feedback fragment for one field.
    """
    if not message:
        return None

    cls = kwargs.pop("cls", "")
    state_cls = {
        "invalid": "invalid-feedback d-block",
        "valid": "valid-feedback d-block",
        "neutral": "form-text text-muted",
    }[state]
    attrs: dict[str, Any] = {"cls": f"{state_cls} {cls}".strip()}
    attrs.update(convert_attrs(kwargs))
    return Div(message, **attrs)

faststrap.components.forms.errors.LiveValidationField(input_element, validate_url, *, label=None, help_text=None, error=None, success=None, is_invalid=False, is_valid=False, required=False, method='post', trigger='blur changed delay:300ms', target='closest .mb-3', swap='outerHTML', indicator=None, **kwargs)

FormGroup wrapper that wires an input for HTMX live validation.

The validation endpoint should return a replacement FormGroup or another fragment compatible with the configured hx_target/hx_swap.

Source code in src/faststrap/components/forms/errors.py
@register(category="forms")
def LiveValidationField(
    input_element: Any,
    validate_url: str,
    *,
    label: str | None = None,
    help_text: str | None = None,
    error: str | None = None,
    success: str | None = None,
    is_invalid: bool = False,
    is_valid: bool = False,
    required: bool = False,
    method: Literal["get", "post"] = "post",
    trigger: str = "blur changed delay:300ms",
    target: str = "closest .mb-3",
    swap: str = "outerHTML",
    indicator: str | None = None,
    **kwargs: Any,
) -> Any:
    """FormGroup wrapper that wires an input for HTMX live validation.

    The validation endpoint should return a replacement `FormGroup` or another
    fragment compatible with the configured `hx_target`/`hx_swap`.
    """
    if hasattr(input_element, "attrs"):
        if method == "get":
            input_element.attrs["hx-get"] = validate_url
        else:
            input_element.attrs["hx-post"] = validate_url
        input_element.attrs["hx-trigger"] = trigger
        input_element.attrs["hx-target"] = target
        input_element.attrs["hx-swap"] = swap
        if indicator:
            input_element.attrs["hx-indicator"] = indicator

    return FormGroup(
        input_element,
        label=label,
        help_text=help_text,
        error=error,
        success=success,
        is_invalid=is_invalid,
        is_valid=is_valid,
        required=required,
        **kwargs,
    )