Rich Form
Covers nine field types with server-side validation and a submission summary.
Includes enctype="multipart/form-data" for file uploads.
Live Demo
Template Code
Copy templates/rich_form.stpl into your app.stpl.
Add your save logic inside the if not errors block.
Move the <style> block into your stylesheet.
{#
================================================================
Rich Form (ScribeFramework template prefab)
Demonstrates: text, email, number, textarea, select/dropdown,
radio buttons, checkboxes, date, and file upload.
How to use:
1. Copy this route into your app.stpl
2. Move the <style> block into your stylesheet (or keep it inline)
3. Rename the route path and form fields to suit your needs
4. Add real save logic inside the `if request.method == 'POST'` block
Requires `enctype="multipart/form-data"` on the <form> when
using file uploads. Remove it (and the file field) if not needed.
================================================================
#}
@route('/example-form', methods=['GET', 'POST'])
@no_layout
{$
page_title = "Application Form"
submitted = False
form_data = {}
errors = {}
if request.method == 'POST':
# Collect submitted values
resume_file = request.files.get('resume')
form_data = {
'full_name': request.form.get('full_name', '').strip(),
'email': request.form.get('email', '').strip(),
'age': request.form.get('age', '').strip(),
'department': request.form.get('department', ''),
'priority': request.form.get('priority', ''),
'interests': request.form.getlist('interests'),
'start_date': request.form.get('start_date', ''),
'notes': request.form.get('notes', '').strip(),
'resume': resume_file.filename if resume_file and resume_file.filename else '',
}
# Basic server-side validation
if not form_data['full_name']:
errors['full_name'] = 'Full name is required.'
if not form_data['email']:
errors['email'] = 'Email address is required.'
if not errors:
# TODO: save form_data to database, send email, etc.
submitted = True
$}
<style>
/* ── Rich Form additions ── move this block to your stylesheet ── */
.form-hint {
font-size: 0.875rem;
color: var(--text-muted, #64748b);
margin-top: 0.25rem;
}
.form-error {
font-size: 0.875rem;
color: var(--error-color, #ef4444);
margin-top: 0.25rem;
}
.form-group input.is-invalid,
.form-group textarea.is-invalid,
.form-group select.is-invalid {
border-color: var(--error-color, #ef4444);
}
.form-group input[type="date"],
.form-group input[type="file"] {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color, #e2e8f0);
border-radius: var(--radius-md, 0.5rem);
font-size: 1rem;
font-family: inherit;
}
.form-group input[type="file"] {
padding: 0.5rem;
border-style: dashed;
cursor: pointer;
}
.form-group input[type="file"]:hover {
border-color: var(--primary-color, #2563eb);
}
.radio-group,
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.25rem;
}
.radio-option,
.checkbox-option {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-weight: normal;
}
.radio-option input,
.checkbox-option input {
width: auto;
cursor: pointer;
accent-color: var(--primary-color, #2563eb);
}
.form-divider {
border: none;
border-top: 1px solid var(--border-color, #e2e8f0);
margin: 1.5rem 0;
}
/* ──────────────────────────────────────────────────────────── */
</style>
<div class="container-narrow">
{% if submitted %}
<div class="card">
<h2>Submission Received</h2>
<p>Thank you, <strong>{{ form_data.full_name }}</strong>! Here's what we got:</p>
<dl style="margin-top: 1rem;">
{% set labels = [
('full_name', 'Full Name'),
('email', 'Email'),
('age', 'Age'),
('department', 'Department'),
('priority', 'Priority'),
('start_date', 'Available From'),
('resume', 'Resume'),
('notes', 'Notes'),
] %}
{% for key, label in labels %}
<div style="display:flex; gap:1rem; padding:0.6rem 0; border-bottom:1px solid var(--border-color)">
<dt style="font-weight:600; min-width:140px; color:var(--text-muted)">{{ label }}</dt>
<dd>{{ form_data[key] if form_data[key] else '—' }}</dd>
</div>
{% endfor %}
<div style="display:flex; gap:1rem; padding:0.6rem 0">
<dt style="font-weight:600; min-width:140px; color:var(--text-muted)">Interests</dt>
<dd>{{ form_data.interests | join(', ') if form_data.interests else '—' }}</dd>
</div>
</dl>
<a href="{{ request.path }}" class="btn btn-secondary" style="margin-top:1.5rem">← Submit another</a>
</div>
{% else %}
<div class="card">
<h2>Application Form</h2>
{% if errors %}
<div class="alert alert-error">Please correct the errors below.</div>
{% endif %}
<form method="POST" enctype="multipart/form-data">
{{ csrf() }}
{# ── Text ──────────────────────────────────────────── #}
<div class="form-group">
<label for="full_name">
Full Name <span style="color:var(--error-color)">*</span>
</label>
<input type="text"
id="full_name"
name="full_name"
value="{{ request.form.get('full_name', '') }}"
class="{{ 'is-invalid' if errors.get('full_name') }}"
required
autofocus>
{% if errors.get('full_name') %}
<div class="form-error">{{ errors.full_name }}</div>
{% endif %}
</div>
{# ── Email ─────────────────────────────────────────── #}
<div class="form-group">
<label for="email">
Email Address <span style="color:var(--error-color)">*</span>
</label>
<input type="email"
id="email"
name="email"
value="{{ request.form.get('email', '') }}"
class="{{ 'is-invalid' if errors.get('email') }}"
required>
{% if errors.get('email') %}
<div class="form-error">{{ errors.email }}</div>
{% else %}
<div class="form-hint">We'll never share your email.</div>
{% endif %}
</div>
{# ── Number ────────────────────────────────────────── #}
<div class="form-group">
<label for="age">Age</label>
<input type="number"
id="age"
name="age"
min="18"
max="120"
value="{{ request.form.get('age', '') }}"
style="max-width: 120px;">
</div>
<hr class="form-divider">
{# ── Select / Dropdown ─────────────────────────────── #}
<div class="form-group">
<label for="department">Department</label>
<select id="department" name="department">
<option value="">— Select a department —</option>
{% for val, label in [
('engineering', 'Engineering'),
('design', 'Design'),
('marketing', 'Marketing'),
('operations', 'Operations'),
] %}
<option value="{{ val }}"
{{ 'selected' if request.form.get('department') == val }}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
{# ── Radio Buttons ─────────────────────────────────── #}
<div class="form-group">
<label>Priority</label>
<div class="radio-group">
{% for val, label in [('low','Low'), ('medium','Medium'), ('high','High')] %}
<label class="radio-option">
<input type="radio"
name="priority"
value="{{ val }}"
{{ 'checked' if request.form.get('priority') == val }}>
{{ label }}
</label>
{% endfor %}
</div>
</div>
{# ── Checkboxes ────────────────────────────────────── #}
<div class="form-group">
<label>Interests</label>
<div class="checkbox-group">
{% for val, label in [
('frontend', 'Frontend'),
('backend', 'Backend'),
('devops', 'DevOps'),
('data', 'Data / Analytics'),
] %}
<label class="checkbox-option">
<input type="checkbox"
name="interests"
value="{{ val }}"
{{ 'checked' if val in request.form.getlist('interests') }}>
{{ label }}
</label>
{% endfor %}
</div>
</div>
<hr class="form-divider">
{# ── Date ──────────────────────────────────────────── #}
<div class="form-group">
<label for="start_date">Available From</label>
<input type="date"
id="start_date"
name="start_date"
value="{{ request.form.get('start_date', '') }}"
style="max-width: 200px;">
</div>
{# ── File Upload ───────────────────────────────────── #}
<div class="form-group">
<label for="resume">Resume</label>
<input type="file"
id="resume"
name="resume"
accept=".pdf,.doc,.docx">
<div class="form-hint">PDF or Word document, max 5 MB.</div>
</div>
{# ── Textarea ──────────────────────────────────────── #}
<div class="form-group">
<label for="notes">Additional Notes</label>
<textarea id="notes"
name="notes"
rows="4"
placeholder="Anything else you'd like us to know?">{{ request.form.get('notes', '') }}</textarea>
</div>
<button type="submit" class="btn btn-primary btn-block">Submit Application</button>
</form>
</div>
{% endif %}
</div>