{# Form Component Macros #}
{# Declarative form building with HTMX support and validation #}
{# Start a form with automatic CSRF protection #}
{% macro form_start(action, method="post", htmx=true, target="", swap="innerHTML", class="") %}
<form action="{{ action }}"
method="post"
{% if class %}class="{{ class }}"{% endif %}
{% if htmx %}
{% if method == "post" %}hx-post="{{ action }}"
{% elif method == "put" %}hx-put="{{ action }}"
{% elif method == "delete" %}hx-delete="{{ action }}"
{% endif %}
{% if target %}hx-target="{{ target }}"{% endif %}
hx-swap="{{ swap }}"
{% endif %}>
{# CSRF token will be injected by middleware #}
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
{% if method != "post" and method != "get" %}
<input type="hidden" name="_method" value="{{ method }}">
{% endif %}
{% endmacro %}
{% macro form_end() %}
</form>
{% endmacro %}
{# Text input field with label and validation #}
{% macro text_field(name, label="", value="", placeholder="", required=false, errors=[]) %}
<div class="form-group {% if errors %}has-error{% endif %}">
{% if label %}
<label for="{{ name }}" class="form-label">
{{ label }}
{% if required %}<span class="required">*</span>{% endif %}
</label>
{% endif %}
<input type="text"
id="{{ name }}"
name="{{ name }}"
class="form-control {% if errors %}is-invalid{% endif %}"
{% if value %}value="{{ value }}"{% endif %}
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
{% if required %}required{% endif %}>
{% if errors %}
<div class="invalid-feedback">
{% for error in errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{# Email input field #}
{% macro email_field(name, label="Email", value="", required=true, errors=[]) %}
<div class="form-group {% if errors %}has-error{% endif %}">
{% if label %}
<label for="{{ name }}" class="form-label">
{{ label }}
{% if required %}<span class="required">*</span>{% endif %}
</label>
{% endif %}
<input type="email"
id="{{ name }}"
name="{{ name }}"
class="form-control {% if errors %}is-invalid{% endif %}"
{% if value %}value="{{ value }}"{% endif %}
{% if required %}required{% endif %}
autocomplete="email">
{% if errors %}
<div class="invalid-feedback">
{% for error in errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{# Password input field #}
{% macro password_field(name, label="Password", required=true, errors=[], autocomplete="current-password") %}
<div class="form-group {% if errors %}has-error{% endif %}">
{% if label %}
<label for="{{ name }}" class="form-label">
{{ label }}
{% if required %}<span class="required">*</span>{% endif %}
</label>
{% endif %}
<input type="password"
id="{{ name }}"
name="{{ name }}"
class="form-control {% if errors %}is-invalid{% endif %}"
{% if required %}required{% endif %}
autocomplete="{{ autocomplete }}">
{% if errors %}
<div class="invalid-feedback">
{% for error in errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{# Textarea field #}
{% macro textarea_field(name, label="", value="", placeholder="", rows=4, required=false, errors=[]) %}
<div class="form-group {% if errors %}has-error{% endif %}">
{% if label %}
<label for="{{ name }}" class="form-label">
{{ label }}
{% if required %}<span class="required">*</span>{% endif %}
</label>
{% endif %}
<textarea id="{{ name }}"
name="{{ name }}"
class="form-control {% if errors %}is-invalid{% endif %}"
rows="{{ rows }}"
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
{% if required %}required{% endif %}>{% if value %}{{ value }}{% endif %}</textarea>
{% if errors %}
<div class="invalid-feedback">
{% for error in errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{# Select dropdown field #}
{% macro select_field(name, label="", options=[], selected="", required=false, errors=[]) %}
<div class="form-group {% if errors %}has-error{% endif %}">
{% if label %}
<label for="{{ name }}" class="form-label">
{{ label }}
{% if required %}<span class="required">*</span>{% endif %}
</label>
{% endif %}
<select id="{{ name }}"
name="{{ name }}"
class="form-control {% if errors %}is-invalid{% endif %}"
{% if required %}required{% endif %}>
{% for option in options %}
<option value="{{ option.value }}"
{% if option.value == selected %}selected{% endif %}>
{{ option.label }}
</option>
{% endfor %}
</select>
{% if errors %}
<div class="invalid-feedback">
{% for error in errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{# Checkbox field #}
{% macro checkbox_field(name, label="", checked=false, value="1", errors=[]) %}
<div class="form-check {% if errors %}has-error{% endif %}">
<input type="checkbox"
id="{{ name }}"
name="{{ name }}"
class="form-check-input {% if errors %}is-invalid{% endif %}"
value="{{ value }}"
{% if checked %}checked{% endif %}>
{% if label %}
<label for="{{ name }}" class="form-check-label">
{{ label }}
</label>
{% endif %}
{% if errors %}
<div class="invalid-feedback">
{% for error in errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{# Radio button field #}
{% macro radio_field(name, label="", options=[], selected="", errors=[]) %}
<div class="form-group {% if errors %}has-error{% endif %}">
{% if label %}
<label class="form-label">{{ label }}</label>
{% endif %}
{% for option in options %}
<div class="form-check">
<input type="radio"
id="{{ name }}_{{ option.value }}"
name="{{ name }}"
class="form-check-input {% if errors %}is-invalid{% endif %}"
value="{{ option.value }}"
{% if option.value == selected %}checked{% endif %}>
<label for="{{ name }}_{{ option.value }}" class="form-check-label">
{{ option.label }}
</label>
</div>
{% endfor %}
{% if errors %}
<div class="invalid-feedback">
{% for error in errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{# Submit button #}
{% macro submit_button(text="Submit", class="btn btn-primary", loading_text="Submitting...") %}
<button type="submit"
class="{{ class }}"
hx-disabled-elt="this"
hx-indicator="#submit-indicator">
<span class="button-text">{{ text }}</span>
<span id="submit-indicator" class="htmx-indicator">{{ loading_text }}</span>
</button>
{% endmacro %}
{# Button with HTMX action #}
{% macro action_button(text, url, method="post", target="", confirm="", class="btn btn-secondary") %}
<button type="button"
class="{{ class }}"
{% if method == "post" %}hx-post="{{ url }}"
{% elif method == "get" %}hx-get="{{ url }}"
{% elif method == "put" %}hx-put="{{ url }}"
{% elif method == "delete" %}hx-delete="{{ url }}"
{% endif %}
{% if target %}hx-target="{{ target }}"{% endif %}
{% if confirm %}hx-confirm="{{ confirm }}"{% endif %}
hx-disabled-elt="this">
{{ text }}
</button>
{% endmacro %}
{# Hidden field #}
{% macro hidden_field(name, value) %}
<input type="hidden" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{# Form styles #}
<style>
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-label .required {
color: #dc3545;
margin-left: 0.25rem;
}
.form-control {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 0.375rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.form-control.is-invalid {
border-color: #dc3545;
}
.form-control.is-invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.form-check {
margin-bottom: 0.5rem;
}
.form-check-input {
margin-top: 0.25rem;
margin-right: 0.5rem;
}
.invalid-feedback {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
color: #dc3545;
}
.btn {
display: inline-block;
padding: 0.5rem 1rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
text-align: center;
text-decoration: none;
cursor: pointer;
border: 1px solid transparent;
border-radius: 0.375rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
background-color: #0069d9;
border-color: #0062cc;
}
.btn-secondary {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-secondary:hover {
background-color: #5a6268;
border-color: #545b62;
}
.btn:disabled {
opacity: 0.65;
cursor: not-allowed;
}
</style>
{% endmacro %}