{#- chirp-ui: Form field macros Extended field macros with BEM styling. These complement (and can replace) Chirp's built-in form macros from "chirp/forms". Internal: field_wrapper provides label, hint, error display. Standard fields use it; checkbox/toggle/radio/range/input_group have custom layouts. -#} {% def form(action, method="get", enctype=none, cls="", attrs="", attrs_map=none, hx_get=none, hx_post=none, hx_put=none, hx_patch=none, hx_delete=none, hx_target=none, hx_swap=none, hx_trigger=none, hx_include=none, hx_select=none, hx_select_oob=none, hx_disabled_elt=none, hx_sync=none, hx_ext=none, hx_vals=none, hx_reset_on_success=none) %} {#- hx_reset_on_success: when true, form resets after successful htmx response (2xx). Defaults to true when form has hx-post/put/patch/delete (via params or attrs_map). See https://htmx.org/examples/reset-user-input/ hx_sync: pass explicitly or via attrs_map to prevent double-submit; e.g. "this:replace". See https://htmx.org/docs/#synchronization -#} {% set _has_hx = hx_post or hx_put or hx_patch or hx_delete or (attrs_map or {}).get("hx-post") or (attrs_map or {}).get("hx-put") or (attrs_map or {}).get("hx-patch") or (attrs_map or {}).get("hx-delete") %} {% set _reset = hx_reset_on_success if hx_reset_on_success is not none else _has_hx %} {% set _sync_from_attrs = (attrs_map or {}).get("hx-sync") %} {% set _sync = hx_sync if hx_sync is not none else _sync_from_attrs %}
{% slot %}
{% end %} {% def fieldset(legend=none, cls="") %}
{% if legend %}{{ legend }}{% end %} {% slot %}
{% end %} {# Shared wrapper: label, slot (control), hint, errors. modifier adds chirpui-field--X. #} {% def field_wrapper(name, label=none, errors=none, required=false, hint=none, modifier="") %}
{% if label %} {% end %} {% slot %} {% if hint and not (errors and errors | field_errors(name)) %} {{ hint }} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} Usage: from "chirpui/forms.html" import text_field, textarea_field, select_field text_field("title", value=form.title, label="Title", errors=errors, required=true) textarea_field("description", value=form.description, label="Description", rows=6) select_field("status", options=statuses, selected=form.status, label="Status") checkbox_field("active", checked=form.active, label="Active") toggle_field("notifications", checked=true, label="Enable notifications") radio_field("plan", options=plans, selected=form.plan, label="Plan") file_field("avatar", label="Avatar", accept="image/*") date_field("birthday", value=form.birthday, label="Birthday") range_field("volume", value=50, min=0, max=100, label="Volume", show_value=true) hidden_field("id", value=form.id) phone_field("phone", value=form.phone, label="Phone") money_field("amount", value=form.amount, label="Amount") masked_field("ssn", mask="999-99-9999", label="SSN") masked_field("expiry", mask="99/99", label="Card expiry") Note: The field_errors filter is provided by Chirp. When using chirp-ui without Chirp, define your own field_errors filter or pass errors=none. -#} {# Text input with label and error display #} {% def text_field(name, value="", label=none, errors=none, type="text", required=false, placeholder="", hint=none, attrs="") %} {% call field_wrapper(name, label, errors, required, hint) %} {% end %} {% end %} {# Password field with secure browser autocomplete defaults #} {% def password_field(name="password", value="", label=none, errors=none, required=true, placeholder="", hint=none, autocomplete="current-password", attrs="") %} {% call field_wrapper(name, label, errors, required, hint) %} {% end %} {% end %} {# Textarea with label and error display #} {% def textarea_field(name, value="", label=none, errors=none, rows=4, required=false, placeholder="", hint=none) %} {% call field_wrapper(name, label, errors, required, hint) %} {% end %} {% end %} {# Select dropdown with label and error display #} {% def select_field(name, options, selected="", label=none, errors=none, required=false, hint=none) %} {% call field_wrapper(name, label, errors, required, hint) %} {% end %} {% end %} {# Checkbox with label #} {% def checkbox_field(name, checked=false, label=none, errors=none) %}
{% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Toggle (switch-style checkbox) #} {# Supports size (sm/md/lg), color variant (success/danger/accent), and label_inside for ON/OFF text #} {% def toggle_field(name, checked=false, label=none, errors=none, size="", variant="", label_inside=false) %} {% set size_class = " chirpui-toggle-wrap--" ~ size if size in ("sm", "lg") else "" %} {% set variant_class = " chirpui-toggle-wrap--" ~ variant if variant in ("success", "danger", "accent") else "" %}
{% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Radio group with label and error display #} {% def radio_field(name, options, selected="", label=none, errors=none, required=false, hint=none, layout="vertical") %}
{% if label %} {{ label }}{% if required %} {% end %} {% end %}
{% for opt in options %} {% end %}
{% if hint and not (errors and errors | field_errors(name)) %} {{ hint }} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Star rating — interactive CSS-only 1-N star picker. Renders radio inputs in reverse DOM order with row-reverse flex so the CSS ~ sibling selector fills stars up to the hovered/checked one. #} {% def star_rating(name, count=5, selected=0, label=none, errors=none, required=false, hint=none, size="") %} {% set size = size | validate_size("star-rating", "") %}
{% if label %} {% end %}
{% for i in range(count, 0, -1) %} {% end %}
{% if hint and not (errors and errors | field_errors(name)) %} {{ hint }} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Thumbs up/down — binary sentiment input (radio pair). #} {% def thumbs(name, selected="", label=none, errors=none, required=false, hint=none, size="") %} {% set size = size | validate_size("thumbs", "") %}
{% if label %} {% end %}
{% if hint and not (errors and errors | field_errors(name)) %} {{ hint }} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Segmented control — connected button-group radio selector. #} {% def segmented_control(name, options, selected="", label=none, errors=none, required=false, hint=none, size="") %} {% set size = size | validate_size("segmented", "") %}
{% if label %} {% end %}
{% for opt in options %} {% end %}
{% if hint and not (errors and errors | field_errors(name)) %} {{ hint }} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Number scale — horizontal numbered radio row (NPS-style 0-10 or 1-5). #} {% def number_scale(name, min=0, max=10, selected=none, label=none, errors=none, required=false, hint=none, low_label="", high_label="") %}
{% if label %} {% end %}
{% for i in range(min, max + 1) %} {% end %}
{% if low_label or high_label %}
{{ low_label }} {{ high_label }}
{% end %} {% if hint and not (errors and errors | field_errors(name)) %} {{ hint }} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# File input with label and error display #} {% def file_field(name, label=none, errors=none, accept="", multiple=false, required=false, hint=none) %} {% call field_wrapper(name, label, errors, required, hint, modifier="file") %} {% end %} {% end %} {# Date input with label and error display #} {% def date_field(name, value="", label=none, errors=none, required=false, min=none, max=none, hint=none) %} {% call field_wrapper(name, label, errors, required, hint) %} {% end %} {% end %} {# Range slider with label and error display #} {% def range_field(name, value=50, min=0, max=100, step=1, label=none, errors=none, hint=none, show_value=false) %}
{% if label or show_value %}
{% if label %} {% end %} {% if show_value %} {{ value }} {% end %}
{% end %} {% if hint and not (errors and errors | field_errors(name)) %} {{ hint }} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Input with prefix/suffix. Use prefix/suffix params for text, or slot prefix/suffix for custom content (icons, etc). #} {% def input_group(name, prefix=none, suffix=none, value="", label=none, errors=none, type="text", required=false, placeholder="", hint=none, attrs="") %}
{% if label %} {% end %}
{% if prefix %} {{ prefix }} {% else %} {% slot prefix %} {% end %} {% if suffix %} {{ suffix }} {% else %} {% slot suffix %} {% end %}
{% if hint and not (errors and errors | field_errors(name)) %} {{ hint }} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Masked text input — uses Alpine Mask plugin. Pass mask (static) or mask_dynamic (expression). Requires @alpinejs/mask loaded before Alpine core. See https://alpinejs.dev/plugins/mask mask: static pattern, e.g. "99/99/9999", "999-99-9999" mask_dynamic: Alpine expression with $input, e.g. "$money($input)" or credit card logic #} {% def masked_field(name, value="", label=none, errors=none, mask=none, mask_dynamic=none, required=false, placeholder="", hint=none, attrs="") %} {% call field_wrapper(name, label, errors, required, hint, modifier="masked") %} {% end %} {% end %} {# Phone field — US format (999) 999-9999 by default. Use masked_field for other formats. #} {% def phone_field(name, value="", label=none, errors=none, format="us", required=false, placeholder="", hint=none, attrs="") %} {% set _mask = "(999) 999-9999" if format == "us" else "9999 999 9999" if format == "uk" else "+9 999 999 9999" if format == "intl" else "(999) 999-9999" %} {% call field_wrapper(name, label, errors, required, hint, modifier="phone") %} {% end %} {% end %} {# Money/currency field — uses Alpine $money(). decimal_sep, thousands_sep, precision optional. $money($input, decimal, thousands, precision) — default ".", ",", 2 #} {% def money_field(name, value="", label=none, errors=none, decimal_sep=".", thousands_sep=",", precision=2, required=false, placeholder="", hint=none, attrs="") %} {% set _money_expr = "$money($input, '" ~ decimal_sep ~ "', '" ~ thousands_sep ~ "', " ~ precision ~ ")" %} {% call field_wrapper(name, label, errors, required, hint, modifier="money") %} {% end %} {% end %} {# Multi-select dropdown with label and error display #} {% def multi_select_field(name, options, selected=none, label=none, errors=none, required=false, hint=none, size=4) %} {% set selected = selected or [] %} {% call field_wrapper(name, label, errors, required, hint) %} {% end %} {% end %} {# Search input with optional htmx debounced search #} {% def search_field(name, value="", label=none, search_url=none, search_target=none, search_trigger="keyup changed delay:300ms", search_include=none, search_sync=none, placeholder="Search...", errors=none, attrs="", attrs_map=none, search_attrs_map=none, search_hx_select=none) %}
{% if label %} {% end %} {% if errors %} {% for msg in errors | field_errors(name) %} {{ msg }} {% end %} {% end %}
{% end %} {# Search bar — composite search input with layout variants. Use inside a form. variant: "solo" (input only, for live search), "with-button" (input + compact submit), "with-icon" (input with ⌕ prefix). With with-button, input flexes; button stays compact. #} {% def search_bar(name, value="", variant="solo", label=none, search_url=none, search_target=none, search_trigger="keyup changed delay:300ms", search_include=none, search_sync=none, placeholder="Search...", button_label="Search", button_icon="⌕", errors=none, attrs="", attrs_map=none, search_attrs_map=none, search_hx_select=none) %} {% end %} {# Key-value form — inline key + value inputs + submit. For "Set config value" etc. -#} {% def key_value_form(action, method="post", key_placeholder="", value_placeholder="", submit_label="Set", key_options=none, key_name="key", value_name="value", attrs="", attrs_map=none, cls="") %} {% call form(action, method=method, attrs=attrs, attrs_map=attrs_map, cls="chirpui-key-value-form" ~ (" " ~ cls if cls else "")) %}
{% if key_options %} {% for opt in key_options %} {% end %} {% else %} {% end %}
{% from "chirpui/button.html" import btn %} {{ btn(submit_label, type="submit", variant="primary") }}
{% end %} {% end %} {# Hidden field #} {% def hidden_field(name, value="") %} {% end %} {# CSRF hidden field helper. Pass token for non-Chirp environments. #} {% def csrf_hidden(token=none, field_name="_csrf_token") %} {% if token is not none %} {% else %} {{ csrf_token() }} {% end %} {% end %} {# Form actions — submit/cancel button row. Use align="end" for right-aligned. #} {% def form_actions(align="start", cls="") %} {% set align_class = " chirpui-form-actions--end" if align == "end" else "" %}
{% slot %}
{% end %}