Forms
Build forms with validation, CSRF protection, and seamless integration with channels and customers
Nimbu provides a comprehensive form system with automatic CSRF protection, validation, file uploads, and integration with channels and customer accounts.
Basic Form Structure
{% form %}
Create forms for any content type:
{% form model %}
{% input 'field_name' %}
{% submit_tag 'Save' %}
{% endform %}Supported Models:
channels.channel_name- Channel entry formscustomer- Customer account formsorder- Order/checkout formspage- Page editable forms (with authentication)
Customer account forms use a name instead of a model, for example {% form login %}. The forms for two-step verification (two_factor, two_factor_start, two_factor_confirm, two_factor_backup_codes, two_factor_disable) are described in Customer Two-Step Verification.
Form Tags
{% input %}
Generate input fields with automatic attributes:
{% input 'name', label: 'Full Name', required: true %}
{% input 'email', as: 'email', placeholder: 'your@email.com' %}
{% input 'phone', as: 'tel', pattern: '[0-9]{10}' %}Input Types (as parameter):
text(default)emailtelurlnumberdatetimedatetime-localpasswordhidden
{% text_area %}
Multi-line text input:
{% text_area 'message', rows: 5, label: 'Your Message' %}
{% text_area 'bio', rows: 10, placeholder: 'Tell us about yourself...' %}{% select %}
Dropdown selection:
{% select 'category', options: 'Option 1,Option 2,Option 3', label: 'Category' %}
{% select 'country', options: countries, label: 'Country' %}{% checkbox %}
Single checkbox:
{% checkbox 'subscribe', label: 'Subscribe to newsletter' %}
{% checkbox 'terms', label: 'I agree to the terms', required: true %}{% file_field %}
File upload:
{% file_field 'photo', label: 'Upload Photo', accept: 'image/*' %}
{% file_field 'document', label: 'Attach File', accept: '.pdf,.doc,.docx' %}File inputs for custom fields
{% input 'field', as: 'file' %} and {% inputs_for_fields %} render file inputs for the file
fields of a channel, customer or product. When the admin chose the field's file types
(allowed_file_types), the input gets an accept attribute with those extensions:
{% form channels.aanvragen %}
{% input 'offerte', as: 'file', label: 'Offerte' %}
{% endform %}<input type="file" name="aanvragen[offerte]" id="aanvragen_offerte" class="input-file" accept=".pdf,.xls,.xlsx,.ods,.csv,.numbers">- A field without chosen file types renders exactly as before, without
accept. - An
accept:you pass in the tag wins over the field's. acceptonly helps the browser's file picker. Nimbu checks the upload on the server either way.
The field hashes in channels.<slug>.custom_fields, submittable_fields and
submittable_html_fields include the same data, so you can show it next to a custom input:
| Key | Present when | Value |
|---|---|---|
max_file_size | the admin set a maximum size | Bytes, e.g. 10485760 |
accept | the admin chose file types | Extensions, e.g. .pdf,.xls,.xlsx,.ods,.csv,.numbers |
{% for field in channels.aanvragen.custom_fields %}
{% if field.name == 'offerte' and field.max_file_size %}
<p class="hint">Max. {{ field.max_file_size | divided_by: 1048576 }} MB</p>
{% endif %}
{% endfor %}Both keys are missing for fields without these settings. The storefront defaults below never show up in them.
Upload limits for visitors
Files that visitors send through a theme form are checked against the field's settings and against the storefront defaults:
- at most 50 MB per file, or the field's own maximum when that is lower
- no SVG files and no "Other" files (
css,js,xml,json, fonts, e-mail files, geo files, Flash) unless the admin allows those categories on the field
A file that breaks a limit gives a field error, which {% error_messages_for %} and the inline
errors show like any other validation error, for example is too large (max. 10 MB) or
file type .svg is not allowed (allowed: Images, PDF). The categories and how admins set them are
described in
Upload limits on file and gallery fields.
What changed for existing themes
Fields that nobody configured used to take any allowed file from visitors, with no size cap of
their own. Now a form that lets visitors upload SVG, JavaScript, fonts or other files from the
"Other" category, or files over 50 MB, gets a field error for those files. To accept them again, the
admin allows the category on the field (Allowed File Types in the field editor). The 50 MB cap for
visitors can't be raised. Theme markup doesn't change: inputs only get accept once an admin picks
file types.
{% submit_tag %}
Submit button:
{% submit_tag 'Send Message' %}
{% submit_tag 'Save', class: 'btn btn-primary btn-lg' %}Validation and Errors
{% error_messages_for %}
Display validation errors:
{% form channels.contact %}
{% error_messages_for form_model %}
{% input 'name', required: true %}
{% input 'email', as: 'email', required: true %}
{% text_area 'message', required: true %}
{% submit_tag 'Send' %}
{% endform %}Custom Error Styling:
{% error_messages_for form_model, class: 'alert alert-danger' %}Practical Examples
Contact Form
<div class="contact-page">
<h2>Get in Touch</h2>
{% form channels.contact, class: 'contact-form' %}
{% error_messages_for form_model %}
<div class="form-row">
<div class="col-md-6">
{% input 'first_name', label: 'First Name', required: true %}
</div>
<div class="col-md-6">
{% input 'last_name', label: 'Last Name', required: true %}
</div>
</div>
<div class="form-group">
{% input 'email', as: 'email', label: 'Email Address', required: true %}
</div>
<div class="form-group">
{% input 'phone', as: 'tel', label: 'Phone Number' %}
</div>
<div class="form-group">
{% select 'subject', options: 'General Inquiry,Support,Sales,Partnership', label: 'Subject', required: true %}
</div>
<div class="form-group">
{% text_area 'message', rows: 6, label: 'Message', required: true %}
</div>
<div class="form-group">
{% checkbox 'subscribe', label: 'Subscribe to our newsletter' %}
</div>
{% submit_tag 'Send Message', class: 'btn btn-primary' %}
{% endform %}
</div>Job Application Form
<div class="job-application">
<h2>Apply for {{ job.title }}</h2>
{% form channels.applications, class: 'application-form' %}
{% error_messages_for form_model %}
<input type="hidden" name="job_id" value="{{ job.id }}">
<h3>Personal Information</h3>
<div class="form-row">
<div class="col-md-6">
{% input 'first_name', label: 'First Name', required: true %}
</div>
<div class="col-md-6">
{% input 'last_name', label: 'Last Name', required: true %}
</div>
</div>
<div class="form-group">
{% input 'email', as: 'email', label: 'Email', required: true %}
</div>
<div class="form-group">
{% input 'phone', as: 'tel', label: 'Phone Number', required: true %}
</div>
<div class="form-group">
{% input 'linkedin', as: 'url', label: 'LinkedIn Profile URL' %}
</div>
<h3>Application Details</h3>
<div class="form-group">
{% file_field 'resume', label: 'Resume (PDF)', accept: '.pdf', required: true %}
</div>
<div class="form-group">
{% file_field 'cover_letter', label: 'Cover Letter (PDF)', accept: '.pdf' %}
</div>
<div class="form-group">
{% text_area 'why_you', rows: 6, label: 'Why do you want to work with us?', required: true %}
</div>
<div class="form-group">
{% input 'start_date', as: 'date', label: 'Available Start Date' %}
</div>
<div class="form-group">
{% checkbox 'gdpr_consent', label: 'I consent to the processing of my personal data', required: true %}
</div>
{% submit_tag 'Submit Application', class: 'btn btn-lg btn-primary' %}
{% endform %}
</div>Customer Registration
<div class="registration-page">
<h2>Create Account</h2>
{% form customer, class: 'registration-form' %}
{% error_messages_for form_model %}
<div class="form-group">
{% input 'email', as: 'email', label: 'Email Address', required: true %}
</div>
<div class="form-group">
{% input 'password', as: 'password', label: 'Password', required: true %}
</div>
<div class="form-group">
{% input 'password_confirmation', as: 'password', label: 'Confirm Password', required: true %}
</div>
<div class="form-row">
<div class="col-md-6">
{% input 'first_name', label: 'First Name', required: true %}
</div>
<div class="col-md-6">
{% input 'last_name', label: 'Last Name', required: true %}
</div>
</div>
<div class="form-group">
{% checkbox 'newsletter', label: 'Send me news and special offers' %}
</div>
<div class="form-group">
{% checkbox 'terms', label: 'I agree to the Terms and Conditions', required: true %}
</div>
{% submit_tag 'Create Account', class: 'btn btn-primary btn-block' %}
{% endform %}
<p class="login-link">
Already have an account? <a href="/login">Sign in</a>
</p>
</div>Newsletter Signup
<section class="newsletter-signup">
<div class="container">
<h3>Stay Updated</h3>
<p>Get the latest news and exclusive offers.</p>
{% form channels.newsletter, class: 'newsletter-form' %}
{% error_messages_for form_model %}
<div class="form-inline">
{% input 'email', as: 'email', placeholder: 'your@email.com', required: true %}
{% submit_tag 'Subscribe', class: 'btn btn-primary' %}
</div>
{% endform %}
</div>
</section>Event Registration with Multiple Attendees
<div class="event-registration">
<h2>Register for {{ event.title }}</h2>
{% form channels.registrations, class: 'registration-form' %}
{% error_messages_for form_model %}
<input type="hidden" name="event_id" value="{{ event.id }}">
<h3>Primary Attendee</h3>
<div class="form-row">
<div class="col-md-6">
{% input 'first_name', label: 'First Name', required: true %}
</div>
<div class="col-md-6">
{% input 'last_name', label: 'Last Name', required: true %}
</div>
</div>
<div class="form-group">
{% input 'email', as: 'email', label: 'Email', required: true %}
</div>
<div class="form-group">
{% input 'company', label: 'Company Name' %}
</div>
<h3>Ticket Information</h3>
<div class="form-group">
{% select 'ticket_type', options: 'Standard,VIP,Student', label: 'Ticket Type', required: true %}
</div>
<div class="form-group">
{% input 'quantity', as: 'number', label: 'Number of Tickets', value: '1', min: '1', max: '10', required: true %}
</div>
<h3>Dietary Requirements</h3>
<div class="form-group">
{% checkbox 'vegetarian', label: 'Vegetarian' %}
{% checkbox 'vegan', label: 'Vegan' %}
{% checkbox 'gluten_free', label: 'Gluten-free' %}
</div>
<div class="form-group">
{% text_area 'special_requests', rows: 3, label: 'Special Requests or Allergies' %}
</div>
{% submit_tag 'Complete Registration', class: 'btn btn-lg btn-success' %}
{% endform %}
</div>Product Review Form
<div class="review-form">
<h3>Write a Review</h3>
{% form channels.reviews, class: 'product-review-form' %}
{% error_messages_for form_model %}
<input type="hidden" name="product_id" value="{{ product.id }}">
<div class="form-group">
<label>Rating *</label>
<div class="rating-input">
{% for i in (1..5) %}
<input type="radio" name="rating" id="rating-{{ i }}" value="{{ i }}" required>
<label for="rating-{{ i }}">ā
</label>
{% endfor %}
</div>
</div>
<div class="form-group">
{% input 'title', label: 'Review Title', required: true %}
</div>
<div class="form-group">
{% text_area 'comment', rows: 5, label: 'Your Review', required: true %}
</div>
<div class="form-group">
{% input 'reviewer_name', label: 'Your Name', required: true %}
</div>
<div class="form-group">
{% input 'reviewer_email', as: 'email', label: 'Email (not published)', required: true %}
</div>
<div class="form-group">
{% checkbox 'verified_purchase', label: 'I purchased this product' %}
</div>
{% submit_tag 'Submit Review', class: 'btn btn-primary' %}
{% endform %}
</div>Advanced Form Patterns
Conditional Fields
{% form channels.bookings %}
{% select 'booking_type', options: 'Individual,Group', label: 'Booking Type' %}
<div class="conditional-fields" data-show-if="booking_type:Group">
{% input 'group_name', label: 'Group Name' %}
{% input 'group_size', as: 'number', label: 'Number of People' %}
</div>
{% submit_tag 'Book Now' %}
{% endform %}
<script>
document.querySelector('[name="booking_type"]').addEventListener('change', function(e) {
const groupFields = document.querySelector('[data-show-if="booking_type:Group"]');
groupFields.style.display = e.target.value === 'Group' ? 'block' : 'none';
});
</script>AJAX Form Submission
{% form channels.contact, class: 'ajax-form', id: 'contact-form' %}
{% input 'name', required: true %}
{% input 'email', as: 'email', required: true %}
{% text_area 'message', required: true %}
<div class="form-status"></div>
{% submit_tag 'Send Message' %}
{% endform %}
<script>
document.getElementById('contact-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const statusDiv = this.querySelector('.form-status');
try {
const response = await fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json'
}
});
if (response.ok) {
statusDiv.innerHTML = '<div class="alert alert-success">Message sent successfully!</div>';
this.reset();
} else {
const errors = await response.json();
statusDiv.innerHTML = `<div class="alert alert-danger">${errors.message}</div>`;
}
} catch (error) {
statusDiv.innerHTML = '<div class="alert alert-danger">An error occurred. Please try again.</div>';
}
});
</script>Best Practices
1. CSRF Protection
All Nimbu forms automatically include CSRF tokens. Never disable this:
<!-- ā
Good: Automatic CSRF protection -->
{% form model %}
<!-- form fields -->
{% endform %}2. Required Field Validation
<!-- ā
Good: Use required attribute -->
{% input 'email', as: 'email', required: true %}
<!-- ā
Good: Client + server validation -->
{% input 'age', as: 'number', min: '18', required: true %}3. Accessible Labels
<!-- ā
Good: Descriptive labels -->
{% input 'phone', as: 'tel', label: 'Phone Number (optional)' %}
<!-- ā Bad: No label -->
<input type="text" name="phone">4. Error Handling
<!-- ā
Good: Show errors clearly -->
{% form model %}
{% error_messages_for form_model, class: 'alert alert-danger' %}
<!-- form fields -->
{% endform %}Next Steps
- Channels - Channel entry forms
- Commerce - Shopping cart forms
- Global Variables - Form drops reference
- Advanced Features - OAuth, integrations
Build powerful, accessible forms with Nimbu!
Multilingual & Localization
Build multilingual sites with locale routing, translations, and localized content
Customer Login with External Providers
Let customers sign in with Microsoft, Facebook, SAML, another Nimbu site or JWT SSO, and build the OAuth2 consent screen when your site is the identity provider